From d17f592c30fe36c73733e7387118e41570e9c394 Mon Sep 17 00:00:00 2001 From: Denis Shilovich Date: Fri, 9 Aug 2024 08:10:44 +0000 Subject: [PATCH] Release 1.7.0 (286) --- .bazelrc | 2 +- .bazelversion | 2 +- .gitlab-ci.yml | 4 + .gitmodules | 12 +- BUILD | 45 - MODULE.bazel | 63 +- MODULE.bazel.lock | 3884 +++++++++++++++-- Package.resolved | 41 +- Package.swift | 3 +- Telegram/BUILD | 1 + .../Sources/NotificationService.swift | 2 +- .../Telegram-iOS/en.lproj/Localizable.strings | 194 + WORKSPACE => WORKSPACE.bzlmod | 86 +- build-system/Make/BuildConfiguration.py | 2 +- build-system/bazel-rules/rules_swift | 2 +- build-system/decrypt.rb | 156 + build-system/tulsi | 1 - ci/fastlane/Fastfile | 41 - ci/fastlane/README.md | 24 - ci/update-spm.sh | 2 +- .../Sources/AccountContext.swift | 165 +- .../Sources/ChatController.swift | 15 +- .../Sources/ChatHistoryLocation.swift | 8 +- .../ContactMultiselectionController.swift | 3 +- .../Sources/ContactSelectionController.swift | 106 + .../Sources/IsMediaStreamable.swift | 4 +- .../AccountContext/Sources/Premium.swift | 14 + .../Sources/AnimatedStickerNode.swift | 2 +- .../Sources/AttachmentContainer.swift | 53 +- .../Sources/AttachmentController.swift | 141 +- .../Sources/AttachmentPanel.swift | 14 +- .../AttachmentUI/Sources/BackButtonNode.swift | 157 + .../AuthorizationSequenceController.swift | 6 +- .../Sources/AvatarVideoNode.swift | 2 +- .../BotCheckoutWebInteractionController.swift | 2 +- ...CheckoutWebInteractionControllerNode.swift | 22 +- submodules/BrowserUI/BUILD | 44 +- .../Sources/BrowserAddressBarComponent.swift | 466 ++ .../Sources/BrowserAddressListComponent.swift | 706 +++ .../BrowserAddressListItemComponent.swift | 364 ++ .../Sources/BrowserBookmarksScreen.swift | 582 +++ .../BrowserUI/Sources/BrowserContent.swift | 121 +- .../Sources/BrowserDocumentContent.swift | 479 ++ ...owserExceptionDomainAlertContentNode.swift | 299 ++ .../Sources/BrowserInstantPageContent.swift | 2389 ++++++---- .../BrowserNavigationBarComponent.swift | 153 +- .../BrowserUI/Sources/BrowserPdfContent.swift | 472 ++ .../Sources/BrowserRecentlyVisited.swift | 88 + .../BrowserUI/Sources/BrowserScreen.swift | 1209 ++++- .../Sources/BrowserSearchBarComponent.swift | 12 +- .../Sources/BrowserTitleBarComponent.swift | 85 + .../Sources/BrowserToolbarComponent.swift | 110 +- .../BrowserUI/Sources/BrowserWebContent.swift | 1264 +++++- .../Sources/SectionHeaderComponent.swift | 174 + submodules/BrowserUI/Sources/Utils.swift | 154 + submodules/Camera/BUILD | 1 + submodules/Camera/Sources/Camera.swift | 7 +- submodules/Camera/Sources/CameraMetrics.swift | 368 +- submodules/Camera/Sources/VideoRecorder.swift | 6 +- .../Sources/ChatImportActivityScreen.swift | 2 +- .../ChatListUI/Sources/ChatContextMenus.swift | 696 +-- .../Sources/ChatListController.swift | 23 +- .../Sources/ChatListSearchContainerNode.swift | 28 +- .../ChatListSearchFiltersContainerNode.swift | 3 + .../Sources/ChatListSearchListPaneNode.swift | 422 +- .../ChatListSearchPaneContainerNode.swift | 27 +- .../Sources/ChatListShimmerNode.swift | 2 +- .../Sources/Node/ChatListItem.swift | 8 +- .../Sources/Node/ChatListItemStrings.swift | 2 +- .../Sources/Node/ChatListNoticeItem.swift | 1 + .../ChatPanelInterfaceInteraction.swift | 7 - ...ChatSendMessageActionSheetController.swift | 10 +- .../ChatSendMessageContextScreen.swift | 41 + .../Sources/ChatTextLinkEditController.swift | 2 +- .../Source/Components/Button.swift | 10 +- .../Sources/ReactionButtonListComponent.swift | 42 +- .../Sources/ComposePollScreen.swift | 2 + .../Sources/CreatePollController.swift | 40 +- .../Sources/ContactListNode.swift | 14 +- .../Sources/ContactsSearchContainerNode.swift | 33 +- .../InviteContactsControllerNode.swift | 2 +- .../Sources/ContactsPeerItem.swift | 6 +- .../Sources/DebugController.swift | 14 +- .../DeviceAccess/Sources/DeviceAccess.swift | 41 +- .../Display/Source/ContainerViewLayout.swift | 4 + .../Navigation/MinimizedContainer.swift | 77 +- .../Navigation/NavigationController.swift | 63 +- .../Source/Navigation/NavigationLayout.swift | 9 + .../Navigation/NavigationModalContainer.swift | 26 +- .../Display/Source/PortalSourceView.swift | 7 + submodules/Display/Source/PortalView.swift | 5 + submodules/Display/Source/UIKitUtils.swift | 10 + .../Display/Source/ViewController.swift | 7 +- submodules/DrawingUI/BUILD | 1 + .../Sources/DrawingEntitiesView.swift | 13 + .../Sources/DrawingLinkEntityView.swift | 2 +- .../DrawingUI/Sources/DrawingScreen.swift | 11 +- .../Sources/DrawingTextEntityView.swift | 12 + .../Sources/DrawingWeatherEntityView.swift | 587 +++ .../Public/FFMpegBinding/FFMpegBinding.h | 1 + .../Public/FFMpegBinding/FFMpegLiveMuxer.h | 11 + .../FFMpegBinding/Sources/FFMpegLiveMuxer.m | 185 + .../ChatItemGalleryFooterContentNode.swift | 2 +- .../Sources/GalleryControllerNode.swift | 4 + .../Sources/Items/ChatImageGalleryItem.swift | 99 +- .../Items/UniversalVideoGalleryItem.swift | 58 +- .../BarsComponentController.swift | 2 +- .../Sources/InAppPurchaseManager.swift | 25 +- .../Sources/InstantPageContentNode.swift | 4 +- .../Sources/InstantPageController.swift | 10 - .../Sources/InstantPageControllerNode.swift | 2 +- .../Sources/InstantPageDetailsNode.swift | 4 +- .../InstantPageGalleryController.swift | 2 +- .../Sources/InstantPageImageItem.swift | 4 +- .../Sources/InstantPageMediaPlaylist.swift | 26 +- .../InstantPagePeerReferenceNode.swift | 14 +- .../InstantPagePlayableVideoItem.swift | 2 +- .../InstantPageReferenceController.swift | 4 +- .../Sources/InstantPageScrollableNode.swift | 16 +- .../Sources/InstantPageTextItem.swift | 12 +- .../Sources/InstantPageTheme.swift | 48 +- .../Sources/InvisibleInkDustNode.swift | 16 +- .../Sources/MediaDustNode.swift | 14 +- .../Sources/InviteLinkViewController.swift | 4 +- .../Sources/ItemListAvatarAndNameItem.swift | 6 +- .../Sources/ItemListVenueItem.swift | 19 +- .../Sources/LegacyMediaPickers.swift | 6 +- .../Sources/LegacyPaintStickersContext.swift | 6 +- .../Sources/ListMessageSnippetItemNode.swift | 69 +- .../LocationUI/Sources/LocationMapNode.swift | 1 + .../Sources/LocationPickerController.swift | 47 +- .../LocationPickerControllerNode.swift | 23 +- .../Sources/LocationSearchContainerNode.swift | 46 +- .../Sources/LocationViewController.swift | 1 - submodules/MediaPickerUI/BUILD | 1 + .../Sources/MediaGroupsScreen.swift | 1 + .../Sources/MediaPickerGridItem.swift | 7 +- .../Sources/MediaPickerScreen.swift | 242 +- .../Sources/MediaPickerSelectedListNode.swift | 2 +- .../Sources/MediaPickerTitleView.swift | 73 +- .../MtProtoKit/MTBindKeyMessageService.h | 1 + .../MtProtoKit/Sources/MTApiEnvironment.m | 87 +- .../Sources/MTBindKeyMessageService.m | 4 + submodules/MtProtoKit/Sources/MTContext.m | 2 +- submodules/MtProtoKit/Sources/MTProto.m | 13 +- .../Sources/OpenInOptions.swift | 2 +- .../TwoFactorAuthDataInputScreen.swift | 4 +- .../Sources/PeerAvatarImageGalleryItem.swift | 4 +- .../Sources/PeerInfoAvatarListNode.swift | 4 +- .../Sources/DeviceContactInfoController.swift | 2 +- .../Sources/PhotoResources.swift | 33 +- submodules/PremiumUI/Resources/gift | Bin 21826 -> 0 bytes submodules/PremiumUI/Resources/gift2.scn | Bin 0 -> 69788 bytes submodules/PremiumUI/Resources/star | Bin 59131 -> 0 bytes submodules/PremiumUI/Resources/star2 | Bin 0 -> 67438 bytes submodules/PremiumUI/Resources/star2.scn | Bin 145346 -> 0 bytes .../PremiumUI/Sources/PremiumGiftScreen.swift | 3 +- .../Sources/PremiumIntroScreen.swift | 2 +- .../BubbleSettingsController.swift | 4 +- .../Sources/CachedFaqInstantPage.swift | 6 +- .../Sources/ChangePhoneNumberController.swift | 2 +- .../DataAndStorageSettingsController.swift | 36 +- .../WebBrowserDomainController.swift | 471 ++ .../WebBrowserDomainExceptionItem.swift | 347 ++ .../Data and Storage/WebBrowserItem.swift | 26 +- .../WebBrowserSettingsController.swift | 424 +- .../ForwardPrivacyChatPreviewItem.swift | 2 +- .../SelectivePrivacySettingsController.swift | 10 +- ...ectivePrivacySettingsPeersController.swift | 2 +- .../TextSizeSelectionController.swift | 16 +- .../Themes/ThemePreviewControllerNode.swift | 18 +- .../Themes/ThemeSettingsChatPreviewItem.swift | 4 +- .../Sources/ShareControllerNode.swift | 14 +- .../Sources/ShareLoadingContainerNode.swift | 2 +- .../Sources/ShareSearchContainerNode.swift | 53 +- .../ShareItems/Sources/ShareItems.swift | 4 +- .../Sources/SparseItemGrid.swift | 558 ++- .../Sources/ChannelStatsController.swift | 189 +- .../Sources/MessageStatsController.swift | 4 +- .../Sources/MonetizationBalanceItem.swift | 9 +- .../Sources/StatsOverviewItem.swift | 73 +- .../Sources/TransactionInfoScreen.swift | 6 +- submodules/Svg/PublicHeaders/Svg/Svg.h | 2 +- submodules/Svg/Sources/Svg.m | 32 +- submodules/TelegramApi/Sources/Api0.swift | 26 +- submodules/TelegramApi/Sources/Api10.swift | 280 +- submodules/TelegramApi/Sources/Api11.swift | 486 +-- submodules/TelegramApi/Sources/Api12.swift | 596 ++- submodules/TelegramApi/Sources/Api13.swift | 432 +- submodules/TelegramApi/Sources/Api14.swift | 326 ++ submodules/TelegramApi/Sources/Api2.swift | 92 +- submodules/TelegramApi/Sources/Api23.swift | 52 + submodules/TelegramApi/Sources/Api25.swift | 12 + submodules/TelegramApi/Sources/Api26.swift | 18 +- submodules/TelegramApi/Sources/Api28.swift | 44 +- submodules/TelegramApi/Sources/Api29.swift | 232 +- submodules/TelegramApi/Sources/Api3.swift | 50 + submodules/TelegramApi/Sources/Api30.swift | 176 +- submodules/TelegramApi/Sources/Api31.swift | 160 +- submodules/TelegramApi/Sources/Api32.swift | 182 +- submodules/TelegramApi/Sources/Api33.swift | 190 +- submodules/TelegramApi/Sources/Api34.swift | 248 +- submodules/TelegramApi/Sources/Api35.swift | 138 + submodules/TelegramApi/Sources/Api36.swift | 160 + submodules/TelegramApi/Sources/Api5.swift | 18 +- submodules/TelegramApi/Sources/Api9.swift | 134 +- .../Sources/TelegramBaseController.swift | 2 +- .../Components/MediaStreamComponent.swift | 4 +- .../MediaStreamVideoComponent.swift | 132 +- .../Sources/PresentationGroupCall.swift | 55 +- .../Sources/VoiceChatController.swift | 3 +- .../Account/AccountIntermediateState.swift | 2 +- .../ApiUtils/StoreMessage_Telegram.swift | 10 +- .../ApiUtils/TelegramMediaAction.swift | 9 + .../Sources/ApiUtils/TelegramMediaFile.swift | 8 +- .../Sources/ApiUtils/TelegramUser.swift | 15 +- .../ApiUtils/TelegramUserPresence.swift | 2 +- .../Network/FetchedMediaResource.swift | 35 + .../PendingMessageUploadedContent.swift | 10 +- .../State/AccountStateManagementUtils.swift | 2 +- .../Sources/State/AccountStateManager.swift | 17 + .../Sources/State/AccountViewTracker.swift | 2 +- .../Sources/State/ApplyUpdateMessage.swift | 14 +- .../ManagedSecretChatOutgoingOperations.swift | 8 +- ...ecretChatIncomingDecryptedOperations.swift | 22 +- .../Sources/State/Serialization.swift | 2 +- .../Sources/State/UpdatesApiUtils.swift | 2 +- .../Sources/Statistics/PeerStatistics.swift | 51 +- .../Statistics/RevenueStatistics.swift | 88 +- .../Statistics/StarsRevenueStatistics.swift | 95 +- .../SyncCore/SyncCore_CachedChannelData.swift | 1 + .../SyncCore/SyncCore_CachedUserData.swift | 168 +- .../SyncCore/SyncCore_MediaReference.swift | 114 +- .../SyncCore/SyncCore_Namespaces.swift | 9 + .../SyncCore_TelegramMediaAction.swift | 36 + .../SyncCore/SyncCore_TelegramMediaFile.swift | 15 +- .../SyncCore/SyncCore_TelegramUser.swift | 41 +- .../TelegramEngine/Data/PeersData.swift | 111 + .../Messages/AttachMenuBots.swift | 2 +- .../TelegramEngine/Messages/BotWebView.swift | 44 + .../TelegramEngine/Messages/MediaArea.swift | 19 +- ...OutgoingMessageWithChatContextResult.swift | 2 +- .../Messages/PendingStoryManager.swift | 93 +- .../Messages/SearchMessages.swift | 53 + .../TelegramEngine/Messages/Stories.swift | 254 +- .../Messages/StoryListContext.swift | 727 ++- .../Messages/TelegramEngineMessages.swift | 16 + .../TelegramEngine/Payments/AppStore.swift | 11 +- .../Payments/BotPaymentForm.swift | 14 +- .../TelegramEngine/Payments/Stars.swift | 153 +- .../Payments/TelegramEnginePayments.swift | 4 + .../Peers/ChannelRecommendation.swift | 66 + .../TelegramEngine/Peers/RecentPeers.swift | 81 + .../Peers/TelegramEnginePeers.swift | 20 + .../Peers/UpdateCachedPeerData.swift | 76 +- .../Stickers/ImportStickers.swift | 4 +- .../TelegramCore/Sources/UpdatePeers.swift | 6 +- .../Resources/PresentationResourceKey.swift | 1 + .../PresentationResourcesItemList.swift | 6 + .../PresentationResourcesSettings.swift | 1 + .../Sources/MessageContentKind.swift | 2 +- .../Sources/ServiceMessageStrings.swift | 52 +- .../Sources/TonFormat.swift} | 25 +- .../Sources/WeatherFormat.swift | 44 + submodules/TelegramUI/BUILD | 3 + .../Sources/AdminUserActionsSheet.swift | 25 +- .../Components/Ads/AdsReportScreen/BUILD | 1 + .../Sources/AdsReportScreen.swift | 295 +- .../CameraScreen/Sources/CameraScreen.swift | 21 +- .../ChatContextResultPeekContent.swift | 2 +- .../Components/Chat/ChatEmptyNode/BUILD | 1 + .../ChatEmptyNode/Sources/ChatEmptyNode.swift | 32 +- .../Sources/ChatHistoryEntry.swift | 4 +- ...ChatInlineSearchResultsListComponent.swift | 4 +- .../ChatMessageActionBubbleContentNode.swift | 2 +- .../ChatMessageActionButtonsNode.swift | 12 +- .../ChatMessageAnimatedStickerItemNode.swift | 4 +- .../ChatMessageAttachedContentNode.swift | 2 +- .../Sources/ChatMessageBubbleItemNode.swift | 30 +- .../Chat/ChatMessageForwardInfoNode/BUILD | 1 + .../Sources/ChatMessageForwardInfoNode.swift | 90 +- .../ChatMessageGiftBubbleContentNode.swift | 16 + .../ChatMessageInstantVideoItemNode.swift | 2 +- .../ChatMessageInteractiveFileNode.swift | 4 +- ...atMessageInteractiveInstantVideoNode.swift | 2 +- .../ChatMessageInteractiveMediaNode.swift | 28 +- .../Sources/ChatMessageItemImpl.swift | 8 +- .../Sources/ChatMessageItemView.swift | 2 +- ...ageProfilePhotoSuggestionContentNode.swift | 2 +- .../Sources/ChatMessageReplyInfoNode.swift | 2 +- .../Sources/ChatMessageStickerItemNode.swift | 2 +- .../ChatMessageWebpageBubbleContentNode.swift | 6 +- .../Chat/ChatRecentActionsController/BUILD | 1 + .../ChatRecentActionsControllerNode.swift | 17 +- .../ChatSendAudioMessageContextPreview.swift | 2 +- .../Sources/ChatSendStarsScreen.swift | 334 +- .../Sources/ReplyAccessoryPanelNode.swift | 9 +- .../Sources/ChatControllerInteraction.swift | 8 +- .../ChatTitleView/Sources/ChatTitleView.swift | 7 +- .../Sources/EmojiTextAttachmentView.swift | 22 +- .../EmptyStateIndicatorComponent/BUILD | 1 + .../EmptyStateIndicatorComponent.swift | 162 +- .../Sources/EntityKeyboard.swift | 2 +- .../Sources/GifContext.swift | 2 +- .../Sources/InteractiveTextComponent.swift | 5 +- .../LegacyInstantVideoController.swift | 2 +- .../Drawing/CodableDrawingEntity.swift | 44 +- .../Sources/Drawing/DrawingTextEntity.swift | 12 +- .../Drawing/DrawingWeatherEntity.swift | 193 + .../MediaEditor/Sources/MediaEditor.swift | 29 +- .../Sources/MediaEditorComposerEntity.swift | 10 +- .../Sources/MediaEditorValues.swift | 66 +- .../Components/MediaEditorScreen/BUILD | 2 + .../Sources/CreateLinkScreen.swift | 11 +- .../Sources/EditStories.swift | 37 +- .../Sources/MediaCoverScreen.swift | 609 +++ .../Sources/MediaEditorDrafts.swift | 6 +- .../Sources/MediaEditorScreen.swift | 942 ++-- .../Sources/StickerCutoutOutlineView.swift | 12 +- .../MediaEditorScreen/Sources/Weather.swift | 236 + .../Sources/MediaScrubberComponent.swift | 131 +- .../Components/MiniAppListScreen/BUILD | 39 + .../Sources/MiniAppListScreen.swift | 809 ++++ .../Components/MinimizedContainer/BUILD | 5 +- .../Sources/MinimizedContainer.swift | 359 +- .../Sources/MinimizedHeaderNode.swift | 205 +- .../MinimizedContainer/Sources/Utils.swift | 5 +- .../Sources/MultiAnimationRenderer.swift | 3 + .../Components/NavigationStackComponent/BUILD | 21 + .../Sources/NavigationStackComponent.swift | 297 ++ .../Sources/EmojiSelectionComponent.swift | 2 +- .../Sources/PeerAllowedReactionsScreen.swift | 2 +- .../Sources/PeerInfoPaneNode.swift | 1 + .../Components/PeerInfo/PeerInfoScreen/BUILD | 3 + .../ListItems/PeerInfoScreenActionItem.swift | 3 +- .../ListItems/PeerInfoScreenAddressItem.swift | 2 +- .../PeerInfoScreenBirthdatePickerItem.swift | 2 +- .../PeerInfoScreenBusinessHoursItem.swift | 3 +- .../PeerInfoScreenCallListItem.swift | 2 +- .../ListItems/PeerInfoScreenCommentItem.swift | 3 +- .../PeerInfoScreenContactInfoItem.swift | 2 +- ...nfoScreenDisclosureEncryptionKeyItem.swift | 3 +- .../PeerInfoScreenDisclosureItem.swift | 27 +- .../ListItems/PeerInfoScreenHeaderItem.swift | 3 +- .../ListItems/PeerInfoScreenInfoItem.swift | 2 +- .../PeerInfoScreenLabeledValueItem.swift | 87 +- .../ListItems/PeerInfoScreenMemberItem.swift | 2 +- .../PeerInfoScreenPersonalChannelItem.swift | 4 +- .../ListItems/PeerInfoScreenSwitchItem.swift | 3 +- .../Sources/Panes/PeerInfoListPaneNode.swift | 2 +- ...PeerInfoAvatarTransformContainerNode.swift | 2 +- .../PeerInfoScreen/Sources/PeerInfoData.swift | 166 +- .../Sources/PeerInfoEditingAvatarNode.swift | 2 +- .../Sources/PeerInfoPaneContainerNode.swift | 43 +- .../Sources/PeerInfoScreen.swift | 1022 +++-- .../PeerInfoScreenMultilineInputtem.swift | 3 +- .../PeerInfo/PeerInfoStoryGridScreen/BUILD | 1 + .../Sources/PeerInfoStoryGridScreen.swift | 2 +- .../Sources/StorySearchGridScreen.swift | 1 - .../PeerInfoVisualMediaPaneNode/BUILD | 2 + .../Sources/PeerInfoStoryPaneNode.swift | 1112 ++++- .../Sources/PeerInfoVisualMediaPaneNode.swift | 7 + .../Sources/PeerSelectionControllerNode.swift | 5 +- .../Sources/GiftAvatarComponent.swift | 186 +- .../Sources/PremiumGiftAttachmentScreen.swift | 35 +- .../Sources/FetchVideoMediaResource.swift | 2 + .../Components/SaveProgressScreen/BUILD | 24 + .../Sources/SaveProgressScreen.swift | 0 .../Sources/SendInviteLinkScreen.swift | 3 +- .../Sources/QuickReplySetupScreen.swift | 1 + .../Settings/LanguageSelectionScreen/BUILD | 31 + .../Sources/LanguageSelectionScreen.swift | 178 + .../Sources/LanguageSelectionScreenNode.swift | 568 +++ .../PeerNameColorChatPreviewItem.swift | 4 +- .../Sources/PeerSelectionLoadingView.swift | 2 +- .../Sources/ReactionChatPreviewItem.swift | 2 +- .../ThemeAccentColorControllerNode.swift | 16 +- .../Sources/WallpaperGalleryItem.swift | 4 +- .../Sources/ThemeColorsGridController.swift | 33 +- .../Sources/CoverListItemComponent.swift | 144 + .../Sources/ShareWithPeersScreen.swift | 249 +- .../Sources/SliderComponent.swift | 16 +- .../TelegramUI/Components/SpaceWarpView/BUILD | 21 + .../SpaceWarpView/STCMeshView/BUILD | 23 + .../PublicHeaders/STCMeshView/STCMeshLayer.h | 58 + .../PublicHeaders/STCMeshView/STCMeshView.h | 26 + .../STCMeshView/Sources/STCMeshLayer.m | 337 ++ .../STCMeshView/Sources/STCMeshView.m | 126 + .../SpaceWarpView/Sources/SpaceWarpView.swift | 547 +++ .../Sources/StarsAvatarComponent.swift | 2 +- .../Stars/StarsImageComponent/BUILD | 2 + .../Sources/StarsImageComponent.swift | 68 +- .../Sources/StarsPurchaseScreen.swift | 458 +- .../Stars/StarsTransactionScreen/BUILD | 2 + .../Sources/StarsTransactionScreen.swift | 400 +- .../Sources/StarsBalanceComponent.swift | 29 +- .../Sources/StarsOverviewItemComponent.swift | 3 +- .../Sources/StarsStatisticsScreen.swift | 17 +- .../StarsTransactionsListPanelComponent.swift | 35 +- .../Sources/StarsTransactionsScreen.swift | 151 +- .../Sources/StarsUtils.swift | 6 - .../Stars/StarsTransferScreen/BUILD | 3 +- .../Sources/StarsTransferScreen.swift | 43 +- .../Sources/StarsWithdrawalScreen.swift | 11 +- .../Components/StickerPickerScreen/BUILD | 3 + .../Sources/StickerPickerScreen.swift | 458 +- .../Sources/StorageUsageScreen.swift | 4 +- .../Stories/StoryContainerScreen/BUILD | 1 + .../Sources/OpenStories.swift | 3 +- .../Sources/StoryAuthorInfoComponent.swift | 22 +- .../Sources/StoryChatContent.swift | 14 +- .../Sources/StoryContainerScreen.swift | 22 +- .../Sources/StoryItemOverlaysView.swift | 209 +- .../StoryItemSetContainerComponent.swift | 856 ++-- ...StoryItemSetContainerViewSendMessage.swift | 8 +- .../Sources/StoryFooterPanelComponent.swift | 10 +- .../Sources/TabSelectorComponent.swift | 41 +- .../Sources/TextLoadingEffect.swift | 30 + .../Components/VideoMessageCameraScreen/BUILD | 1 + .../Sources/VideoMessageCameraScreen.swift | 299 +- .../VideoMessageFlash.imageset}/Contents.json | 2 +- .../VideoMessageFlash.imageset/flash.pdf | Bin 0 -> 1549 bytes .../Privacy.imageset/Contents.json | 12 + .../Privacy.imageset/privacy (2).pdf | Bin 0 -> 2500 bytes .../ReadingList.imageset/Contents.json | 12 + .../ReadingList.imageset/readinglist.pdf | Bin 0 -> 1189 bytes .../Bookmark.imageset/Bookmark.pdf | Bin 0 -> 2667 bytes .../Bookmark.imageset/Contents.json | 12 + .../Instant View/Browser.imageset/Browser.pdf | Bin 0 -> 2160 bytes .../Browser.imageset/Contents.json | 12 + .../Instant View/CloseIcon.imageset/Close.pdf | Bin 0 -> 1428 bytes .../CloseIcon.imageset/Contents.json | 12 + .../Instant View/Contents.json | 6 +- .../NewTab.imageset/Contents.json | 12 + .../Instant View/NewTab.imageset/newtab.pdf | Bin 0 -> 2549 bytes .../Browser.imageset/ic_lt_safari.pdf | Bin 4345 -> 0 bytes .../Settings/Reload.imageset/Contents.json | 12 + .../Settings/Reload.imageset/scheduled.pdf | Bin 0 -> 1755 bytes .../Location/GoTo.imageset/Contents.json | 12 + .../Location/GoTo.imageset/goto.pdf | Bin 0 -> 1739 bytes .../SelectedIcon.imageset/Contents.json | 12 + .../SelectedIcon.imageset/albumcheck.pdf | Bin 0 -> 1479 bytes .../Premium/Stars/Gift.imageset/Contents.json | 12 + .../Stars/Gift.imageset/giftstars_24.pdf | Bin 0 -> 9544 bytes .../Menu/Balance.imageset/Contents.json | 12 + .../Menu/Balance.imageset/balance_30.pdf | Bin 0 -> 3506 bytes .../EditCover.imageset/Contents.json | 12 + .../EditCover.imageset/storycover_24.pdf | Bin 0 -> 2441 bytes .../Resources/Animations/PremiumRequired.tgs | Bin 0 -> 1676 bytes .../Resources/Animations/StarsBuy.tgs | Bin 0 -> 11142 bytes .../Resources/Animations/StarsSend.tgs | Bin 0 -> 5796 bytes .../Resources/Animations/roundFlash_off.json | 1 + .../Resources/Animations/roundFlash_on.json | 1 + .../Resources/WebEmbed/UIWebViewSearch.js | 52 +- .../TelegramUI/Resources/currencies.json | 11 +- .../TelegramUI/Sources/AppDelegate.swift | 2 +- .../Sources/ApplicationContext.swift | 19 +- .../Sources/AttachmentFileController.swift | 39 +- .../Chat/ChatControllerLoadDisplayNode.swift | 12 +- .../ChatControllerNavigateToMessage.swift | 190 +- .../ChatControllerOpenLinkContextMenu.swift | 2 +- .../Chat/ChatControllerOpenStorySharing.swift | 98 - .../Chat/ChatControllerOpenWebApp.swift | 791 ++-- .../Sources/Chat/ChatControllerPaste.swift | 3 +- ...ChatMessageDisplaySendMessageOptions.swift | 7 +- .../TelegramUI/Sources/ChatController.swift | 94 +- .../Sources/ChatControllerAdminBanUsers.swift | 40 +- .../Sources/ChatControllerNode.swift | 108 +- .../ChatControllerOpenAttachmentMenu.swift | 2 +- .../ChatControllerOpenCalendarSearch.swift | 2 +- ...rollerOpenMessageReactionContextMenu.swift | 10 +- .../ChatControllerOpenMessageReplies.swift | 4 +- ...ChatControllerScrollToPointInHistory.swift | 4 +- .../Sources/ChatHistoryEntriesForView.swift | 97 +- .../Sources/ChatHistoryListNode.swift | 56 +- .../Sources/ChatHistoryViewForLocation.swift | 16 +- .../ChatInterfaceStateContextMenus.swift | 14 +- .../ChatSearchResultsContollerNode.swift | 2 +- .../TelegramUI/Sources/ChatThemeScreen.swift | 2 +- .../ContactMultiselectionController.swift | 6 +- .../ContactMultiselectionControllerNode.swift | 10 +- .../Sources/ContactSelectionController.swift | 30 +- .../ContactSelectionControllerNode.swift | 51 +- .../Sources/FetchCachedRepresentations.swift | 4 +- ...ListContextResultsChatInputPanelItem.swift | 2 +- .../Sources/NavigateToChatController.swift | 6 +- .../TelegramUI/Sources/OpenChatMessage.swift | 29 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 19 +- submodules/TelegramUI/Sources/OpenUrl.swift | 142 +- .../OverlayAudioPlayerControllerNode.swift | 6 +- .../Sources/PeerMessagesMediaPlaylist.swift | 4 +- .../Sources/PollResultsController.swift | 2 +- .../PreparedChatHistoryViewTransition.swift | 2 +- .../Sources/SharedAccountContext.swift | 288 +- .../Sources/TelegramRootController.swift | 128 +- .../TelegramUI/Sources/TextLinkHandling.swift | 9 +- .../Sources/ExperimentalUISettings.swift | 12 +- .../InstantPagePresentationSettings.swift | 2 + .../Sources/MediaAutoDownloadSettings.swift | 10 + .../Sources/PostboxKeys.swift | 2 + .../Sources/WebBrowserSettings.swift | 54 +- .../Sources/NativeVideoContent.swift | 4 + submodules/TelegramVoip/BUILD | 1 + submodules/TelegramVoip/Package.swift | 2 + .../WrappedMediaStreamingContext.swift | 462 ++ .../Sources/ChatTextInputAttributes.swift | 1 + .../Sources/LocalizationListItem.swift | 19 +- .../Sources/UndoOverlayControllerNode.swift | 5 +- submodules/UrlEscaping/Sources/Punycode.swift | 317 ++ .../UrlEscaping/Sources/UrlEscaping.swift | 10 +- .../UrlHandling/Sources/UrlHandling.swift | 88 +- submodules/Utils/DeviceModel/BUILD | 20 + .../DeviceModel/Sources/DeviceModel.swift | 368 ++ .../WatchBridge/Sources/WatchBridge.swift | 2 +- .../Sources/WebSearchController.swift | 23 - .../Sources/WebSearchGalleryController.swift | 2 +- submodules/WebUI/BUILD | 10 +- .../WebUI/Sources/WebAppController.swift | 482 +- .../WebUI/Sources/WebAppTitleView.swift | 159 + submodules/WebUI/Sources/WebAppWebView.swift | 4 + .../Sources/WidgetItemsUtils.swift | 2 +- .../Sources/FFMpeg/build-ffmpeg-bazel.sh | 8 +- swift_deps.bzl | 322 -- swift_deps_index.json | 2748 ------------ versions.json | 2 +- 525 files changed, 36719 insertions(+), 12277 deletions(-) rename WORKSPACE => WORKSPACE.bzlmod (59%) create mode 100644 build-system/decrypt.rb delete mode 160000 build-system/tulsi create mode 100644 submodules/AttachmentUI/Sources/BackButtonNode.swift create mode 100644 submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift create mode 100644 submodules/BrowserUI/Sources/BrowserAddressListComponent.swift create mode 100644 submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift create mode 100644 submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift create mode 100644 submodules/BrowserUI/Sources/BrowserDocumentContent.swift create mode 100644 submodules/BrowserUI/Sources/BrowserExceptionDomainAlertContentNode.swift create mode 100644 submodules/BrowserUI/Sources/BrowserPdfContent.swift create mode 100644 submodules/BrowserUI/Sources/BrowserRecentlyVisited.swift create mode 100644 submodules/BrowserUI/Sources/BrowserTitleBarComponent.swift create mode 100644 submodules/BrowserUI/Sources/SectionHeaderComponent.swift create mode 100644 submodules/BrowserUI/Sources/Utils.swift create mode 100644 submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift create mode 100644 submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegLiveMuxer.h create mode 100644 submodules/FFMpegBinding/Sources/FFMpegLiveMuxer.m delete mode 100644 submodules/PremiumUI/Resources/gift create mode 100644 submodules/PremiumUI/Resources/gift2.scn delete mode 100644 submodules/PremiumUI/Resources/star create mode 100644 submodules/PremiumUI/Resources/star2 delete mode 100644 submodules/PremiumUI/Resources/star2.scn create mode 100644 submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift create mode 100644 submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift rename submodules/{StatisticsUI/Sources/MonetizationUtils.swift => TelegramStringFormatting/Sources/TonFormat.swift} (66%) create mode 100644 submodules/TelegramStringFormatting/Sources/WeatherFormat.swift create mode 100644 submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingWeatherEntity.swift create mode 100644 submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift create mode 100644 submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift create mode 100644 submodules/TelegramUI/Components/MiniAppListScreen/BUILD create mode 100644 submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift create mode 100644 submodules/TelegramUI/Components/NavigationStackComponent/BUILD create mode 100644 submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift create mode 100644 submodules/TelegramUI/Components/SaveProgressScreen/BUILD rename submodules/TelegramUI/Components/{MediaEditorScreen => SaveProgressScreen}/Sources/SaveProgressScreen.swift (100%) create mode 100644 submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/BUILD create mode 100644 submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift create mode 100644 submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift create mode 100644 submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CoverListItemComponent.swift create mode 100644 submodules/TelegramUI/Components/SpaceWarpView/BUILD create mode 100644 submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/BUILD create mode 100644 submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshLayer.h create mode 100644 submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshView.h create mode 100644 submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshLayer.m create mode 100644 submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshView.m create mode 100644 submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift delete mode 100644 submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsUtils.swift rename submodules/TelegramUI/Images.xcassets/{Instant View/Settings/Browser.imageset => Camera/VideoMessageFlash.imageset}/Contents.json (75%) create mode 100644 submodules/TelegramUI/Images.xcassets/Camera/VideoMessageFlash.imageset/flash.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/privacy (2).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReadingList.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReadingList.imageset/readinglist.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Bookmark.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Browser.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Close.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/NewTab.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/NewTab.imageset/newtab.pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/Settings/Browser.imageset/ic_lt_safari.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/Settings/Reload.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/Settings/Reload.imageset/scheduled.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Location/GoTo.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Location/GoTo.imageset/goto.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/SelectedIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/SelectedIcon.imageset/albumcheck.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/Gift.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/Gift.imageset/giftstars_24.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/Balance.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/Balance.imageset/balance_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/Context Menu/EditCover.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/Context Menu/EditCover.imageset/storycover_24.pdf create mode 100644 submodules/TelegramUI/Resources/Animations/PremiumRequired.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/StarsBuy.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/StarsSend.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/roundFlash_off.json create mode 100644 submodules/TelegramUI/Resources/Animations/roundFlash_on.json create mode 100644 submodules/UrlEscaping/Sources/Punycode.swift create mode 100644 submodules/Utils/DeviceModel/BUILD create mode 100644 submodules/Utils/DeviceModel/Sources/DeviceModel.swift create mode 100644 submodules/WebUI/Sources/WebAppTitleView.swift delete mode 100644 swift_deps.bzl delete mode 100644 swift_deps_index.json diff --git a/.bazelrc b/.bazelrc index 43ccf92e1f2..2495727c555 100644 --- a/.bazelrc +++ b/.bazelrc @@ -36,4 +36,4 @@ build --spawn_strategy=standalone build --strategy=SwiftCompile=standalone build --define RULES_SWIFT_BUILD_DUMMY_WORKER=1 -build --noenable_bzlmod +common --enable_bzlmod diff --git a/.bazelversion b/.bazelversion index f9da12e1184..ef09838cb29 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -6.3.2 \ No newline at end of file +7.1.1 \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4a2997b380f..a9d2756c951 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,6 +19,8 @@ internal: except: - tags script: + - export PATH=/opt/homebrew/opt/ruby/bin:$PATH + - export PATH=`gem environment gemdir`/bin:$PATH - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64 - python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-internal.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" environment: @@ -93,6 +95,8 @@ beta_testflight: except: - tags script: + - export PATH=/opt/homebrew/opt/ruby/bin:$PATH + - export PATH=`gem environment gemdir`/bin:$PATH - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appstore-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=appstore --configuration=release_arm64 environment: name: testflight_llc diff --git a/.gitmodules b/.gitmodules index 07d36303b0e..38d4aca6a50 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,25 +3,27 @@ url = https://github.com/TelegramMessenger/rlottie.git [submodule "build-system/bazel-rules/rules_apple"] path = build-system/bazel-rules/rules_apple - url = https://github.com/ali-fareed/rules_apple.git + url = https://github.com/ali-fareed/rules_apple.git [submodule "build-system/bazel-rules/rules_swift"] path = build-system/bazel-rules/rules_swift - url = https://github.com/bazelbuild/rules_swift.git + url = https://github.com/bazelbuild/rules_swift.git [submodule "build-system/bazel-rules/apple_support"] path = build-system/bazel-rules/apple_support url = https://github.com/bazelbuild/apple_support.git [submodule "submodules/TgVoip/libtgvoip"] path = submodules/TgVoip/libtgvoip url = https://github.com/telegramdesktop/libtgvoip.git -[submodule "build-system/tulsi"] - path = build-system/tulsi - url = https://github.com/bazelbuild/tulsi.git [submodule "submodules/TgVoipWebrtc/tgcalls"] path = submodules/TgVoipWebrtc/tgcalls url = https://github.com/TelegramMessenger/tgcalls.git [submodule "third-party/libvpx/libvpx"] path = third-party/libvpx/libvpx url = https://github.com/webmproject/libvpx.git +# We use fork because of video call crash +# https://github.com/denis15yo/webrtc/commit/c75f2a397ec3c7db12677b39b52d2b3f8ee9161e +# When merging with the telegram code, we can try using the original repository +# and check if the crash (pressing the video button during the call) is fixed. +# Perhaps the crash is related to the minimum version of iOS (we have iOS 14, telegram has iOS 12). [submodule "third-party/webrtc/webrtc"] path = third-party/webrtc/webrtc url = https://github.com/denis15yo/webrtc.git diff --git a/BUILD b/BUILD index 1dd4b041db0..e69de29bb2d 100644 --- a/BUILD +++ b/BUILD @@ -1,45 +0,0 @@ -load("@bazel_gazelle//:def.bzl", "gazelle", "gazelle_binary") -load("@rules_swift_package_manager//swiftpkg:defs.bzl", "swift_update_packages") - -# Ignore the `.build` folder that is created by running Swift package manager -# commands. The Swift Gazelle plugin executes some Swift package manager -# commands to resolve external dependencies. This results in a `.build` file -# being created. -# NOTE: Swift package manager is not used to build any of the external packages. -# The `.build` directory should be ignored. Be sure to configure your source -# control to ignore it (i.e., add it to your `.gitignore`). -# gazelle:exclude .build - -# This declaration builds a Gazelle binary that incorporates all of the Gazelle -# plugins for the languages that you use in your workspace. In this example, we -# are only listing the Gazelle plugin for Swift from rules_swift_package_manager. -gazelle_binary( - name = "gazelle_bin", - languages = [ - "@rules_swift_package_manager//gazelle", - ], -) - -# This macro defines two targets: `swift_update_pkgs` and -# `swift_update_pkgs_to_latest`. -# -# The `swift_update_pkgs` target should be run whenever the list of external -# dependencies is updated in the `Package.swift`. Running this target will -# populate the `swift_deps.bzl` with `swift_package` declarations for all of -# the direct and transitive Swift packages that your project uses. -# -# The `swift_update_pkgs_to_latest` target should be run when you want to -# update your Swift dependencies to their latest eligible version. -swift_update_packages( - name = "swift_update_pkgs", - gazelle = ":gazelle_bin", - generate_swift_deps_for_workspace = True, - update_bzlmod_stanzas = False, -) - -# This target updates the Bazel build files for your project. Run this target -# whenever you add or remove source files from your project. -gazelle( - name = "update_build_files", - gazelle = ":gazelle_bin", -) \ No newline at end of file diff --git a/MODULE.bazel b/MODULE.bazel index 00bb18361f7..c1ee18b44d8 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,6 +1,57 @@ -############################################################################### -# Bazel now uses Bzlmod by default to manage external dependencies. -# Please consider migrating your external dependencies from WORKSPACE to MODULE.bazel. -# -# For more details, please check https://github.com/bazelbuild/bazel/issues/18958 -############################################################################### +bazel_dep( + name = "rules_swift_package_manager", + version = "0.36.0", +) + +bazel_dep( + name = "apple_support", + version = "0.0.0", +) +local_path_override( + module_name = "apple_support", + path = "build-system/bazel-rules/apple_support", +) + +bazel_dep( + name = "rules_swift", + version = "0.0.0", + repo_name = "build_bazel_rules_swift", +) +local_path_override( + module_name = "rules_swift", + path = "build-system/bazel-rules/rules_swift", +) + +bazel_dep( + name = "rules_apple", + version = "0.0.0", + repo_name = "build_bazel_rules_apple", +) +local_path_override( + module_name = "rules_apple", + path = "build-system/bazel-rules/rules_apple", +) + +bazel_dep( + name = "rules_xcodeproj", + version = "0.0.0", + dev_dependency = True, +) +local_path_override( + module_name = "rules_xcodeproj", + path = "build-system/bazel-rules/rules_xcodeproj", +) + +swift_deps = use_extension( + "@rules_swift_package_manager//:extensions.bzl", + "swift_deps", +) +swift_deps.from_package( + resolved = "//:Package.resolved", + swift = "//:Package.swift", +) +use_repo( + swift_deps, + "swiftpkg_nicegram_assistant_ios", + "swiftpkg_nicegram_wallet_ios", +) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 7266ea05568..918aaf55be2 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1,6 +1,6 @@ { - "lockFileVersion": 3, - "moduleFileHash": "0e3e315145ac7ee7a4e0ac825e1c5e03c068ec1254dd42c3caaecb27e921dc4d", + "lockFileVersion": 6, + "moduleFileHash": "4612700451c1f3fd981918f8e035be4dd27c67ed74222e65829508c8db91a491", "flags": { "cmdRegistries": [ "https://bcr.bazel.build/" @@ -13,7 +13,11 @@ "compatibilityMode": "ERROR" }, "localOverrideHashes": { - "bazel_tools": "922ea6752dc9105de5af957f7a99a6933c0a6a712d23df6aad16a9c399f7e787" + "rules_apple": "cd087b86f43819d8413b130a554f8ea919826efa9d436218191abc5edf68fe4c", + "apple_support": "8cfbef5c82204246df4ae09be81908e8c48495037c2aac960109136a288caec3", + "bazel_tools": "1ae69322ac3823527337acf02016e8ee95813d8d356f47060255b8956fa642f0", + "rules_swift": "e2ca418271aade7e3c320c6f0163c477ae82a28a46126db80a046ede5e296c69", + "rules_xcodeproj": "6fbfbadd9e80dc8ad14c50e21e794c090fd7f7f528ed9b78feec3d2daa4eaa25" }, "moduleDepGraph": { "": { @@ -23,8 +27,368 @@ "repoName": "", "executionPlatformsToRegister": [], "toolchainsToRegister": [], - "extensionUsages": [], + "extensionUsages": [ + { + "extensionBzlFile": "@rules_swift_package_manager//:extensions.bzl", + "extensionName": "swift_deps", + "usingModule": "", + "location": { + "file": "@@//:MODULE.bazel", + "line": 45, + "column": 27 + }, + "imports": { + "swiftpkg_nicegram_assistant_ios": "swiftpkg_nicegram_assistant_ios", + "swiftpkg_nicegram_wallet_ios": "swiftpkg_nicegram_wallet_ios" + }, + "devImports": [], + "tags": [ + { + "tagName": "from_package", + "attributeValues": { + "resolved": "//:Package.resolved", + "swift": "//:Package.swift" + }, + "devDependency": false, + "location": { + "file": "@@//:MODULE.bazel", + "line": 49, + "column": 24 + } + } + ], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + } + ], + "deps": { + "rules_swift_package_manager": "rules_swift_package_manager@0.36.0", + "apple_support": "apple_support@_", + "build_bazel_rules_swift": "rules_swift@_", + "build_bazel_rules_apple": "rules_apple@_", + "rules_xcodeproj": "rules_xcodeproj@_", + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + } + }, + "rules_swift_package_manager@0.36.0": { + "name": "rules_swift_package_manager", + "version": "0.36.0", + "key": "rules_swift_package_manager@0.36.0", + "repoName": "rules_swift_package_manager", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [], + "extensionUsages": [ + { + "extensionBzlFile": "@apple_support//crosstool:setup.bzl", + "extensionName": "apple_cc_configure_extension", + "usingModule": "rules_swift_package_manager@0.36.0", + "location": { + "file": "https://bcr.bazel.build/modules/rules_swift_package_manager/0.36.0/MODULE.bazel", + "line": 40, + "column": 35 + }, + "imports": { + "local_config_apple_cc": "local_config_apple_cc" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + }, + { + "extensionBzlFile": "@bazel_gazelle//:extensions.bzl", + "extensionName": "go_deps", + "usingModule": "rules_swift_package_manager@0.36.0", + "location": { + "file": "https://bcr.bazel.build/modules/rules_swift_package_manager/0.36.0/MODULE.bazel", + "line": 46, + "column": 24 + }, + "imports": { + "com_github_bazelbuild_buildtools": "com_github_bazelbuild_buildtools", + "com_github_creasty_defaults": "com_github_creasty_defaults", + "com_github_deckarep_golang_set_v2": "com_github_deckarep_golang_set_v2", + "com_github_spf13_cobra": "com_github_spf13_cobra", + "com_github_stretchr_testify": "com_github_stretchr_testify", + "in_gopkg_yaml_v3": "in_gopkg_yaml_v3", + "org_golang_x_exp": "org_golang_x_exp", + "org_golang_x_text": "org_golang_x_text" + }, + "devImports": [], + "tags": [ + { + "tagName": "from_file", + "attributeValues": { + "go_mod": "//:go.mod" + }, + "devDependency": false, + "location": { + "file": "https://bcr.bazel.build/modules/rules_swift_package_manager/0.36.0/MODULE.bazel", + "line": 47, + "column": 18 + } + } + ], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + } + ], + "deps": { + "cgrindel_bazel_starlib": "cgrindel_bazel_starlib@0.21.0", + "bazel_skylib": "bazel_skylib@1.5.0", + "io_bazel_rules_go": "rules_go@0.47.0", + "apple_support": "apple_support@_", + "rules_cc": "rules_cc@0.0.9", + "platforms": "platforms@0.0.9", + "build_bazel_rules_swift": "rules_swift@_", + "build_bazel_rules_apple": "rules_apple@_", + "bazel_gazelle": "gazelle@0.37.0", + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + }, + "repoSpec": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/cgrindel/rules_swift_package_manager/releases/download/v0.36.0/rules_swift_package_manager.v0.36.0.tar.gz" + ], + "integrity": "sha256-3m5u/pXNxFQAKuGvgAARQSnpVPBMYFgFVn/lvG5xV94=", + "strip_prefix": "", + "remote_patches": { + "https://bcr.bazel.build/modules/rules_swift_package_manager/0.36.0/patches/module_dot_bazel_version.patch": "sha256-ivKQmiiCNC9DFlpC53UKdfdVrwRJsZCEpMp9x+Xsu28=" + }, + "remote_patch_strip": 1 + } + } + }, + "apple_support@_": { + "name": "apple_support", + "version": "0", + "key": "apple_support@_", + "repoName": "build_bazel_apple_support", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [ + "@local_config_apple_cc_toolchains//:all" + ], + "extensionUsages": [ + { + "extensionBzlFile": "@build_bazel_apple_support//crosstool:setup.bzl", + "extensionName": "apple_cc_configure_extension", + "usingModule": "apple_support@_", + "location": { + "file": "@@apple_support~//:MODULE.bazel", + "line": 19, + "column": 35 + }, + "imports": { + "local_config_apple_cc": "local_config_apple_cc", + "local_config_apple_cc_toolchains": "local_config_apple_cc_toolchains" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + } + ], + "deps": { + "bazel_skylib": "bazel_skylib@1.5.0", + "platforms": "platforms@0.0.9", + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + } + }, + "rules_swift@_": { + "name": "rules_swift", + "version": "0", + "key": "rules_swift@_", + "repoName": "build_bazel_rules_swift", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [], + "extensionUsages": [ + { + "extensionBzlFile": "@build_bazel_rules_swift//swift:extensions.bzl", + "extensionName": "non_module_deps", + "usingModule": "rules_swift@_", + "location": { + "file": "@@rules_swift~//:MODULE.bazel", + "line": 18, + "column": 32 + }, + "imports": { + "build_bazel_rules_swift_index_import": "build_bazel_rules_swift_index_import", + "build_bazel_rules_swift_local_config": "build_bazel_rules_swift_local_config", + "com_github_apple_swift_log": "com_github_apple_swift_log", + "com_github_apple_swift_nio": "com_github_apple_swift_nio", + "com_github_apple_swift_nio_extras": "com_github_apple_swift_nio_extras", + "com_github_apple_swift_nio_http2": "com_github_apple_swift_nio_http2", + "com_github_apple_swift_nio_transport_services": "com_github_apple_swift_nio_transport_services", + "com_github_apple_swift_protobuf": "com_github_apple_swift_protobuf", + "com_github_grpc_grpc_swift": "com_github_grpc_grpc_swift" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + }, + { + "extensionBzlFile": "@build_bazel_apple_support//crosstool:setup.bzl", + "extensionName": "apple_cc_configure_extension", + "usingModule": "rules_swift@_", + "location": { + "file": "@@rules_swift~//:MODULE.bazel", + "line": 32, + "column": 35 + }, + "imports": { + "local_config_apple_cc": "local_config_apple_cc" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + } + ], + "deps": { + "bazel_features": "bazel_features@1.9.1", + "bazel_skylib": "bazel_skylib@1.5.0", + "build_bazel_apple_support": "apple_support@_", + "rules_cc": "rules_cc@0.0.9", + "platforms": "platforms@0.0.9", + "com_google_protobuf": "protobuf@21.7", + "rules_proto": "rules_proto@5.3.0-21.7", + "com_github_nlohmann_json": "nlohmann_json@3.6.1", + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + } + }, + "rules_apple@_": { + "name": "rules_apple", + "version": "0", + "key": "rules_apple@_", + "repoName": "build_bazel_rules_apple", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [], + "extensionUsages": [ + { + "extensionBzlFile": "@build_bazel_rules_apple//apple:extensions.bzl", + "extensionName": "non_module_deps", + "usingModule": "rules_apple@_", + "location": { + "file": "@@rules_apple~//:MODULE.bazel", + "line": 21, + "column": 32 + }, + "imports": { + "xctestrunner": "xctestrunner" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + }, + { + "extensionBzlFile": "@build_bazel_rules_apple//apple:apple.bzl", + "extensionName": "provisioning_profile_repository_extension", + "usingModule": "rules_apple@_", + "location": { + "file": "@@rules_apple~//:MODULE.bazel", + "line": 27, + "column": 48 + }, + "imports": { + "local_provisioning_profiles": "local_provisioning_profiles" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + }, + { + "extensionBzlFile": "@build_bazel_apple_support//crosstool:setup.bzl", + "extensionName": "apple_cc_configure_extension", + "usingModule": "rules_apple@_", + "location": { + "file": "@@rules_apple~//:MODULE.bazel", + "line": 30, + "column": 35 + }, + "imports": { + "local_config_apple_cc": "local_config_apple_cc" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + } + ], + "deps": { + "build_bazel_apple_support": "apple_support@_", + "bazel_skylib": "bazel_skylib@1.5.0", + "platforms": "platforms@0.0.9", + "build_bazel_rules_swift": "rules_swift@_", + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + } + }, + "rules_xcodeproj@_": { + "name": "rules_xcodeproj", + "version": "0.0.0", + "key": "rules_xcodeproj@_", + "repoName": "rules_xcodeproj", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [], + "extensionUsages": [ + { + "extensionBzlFile": "@rules_xcodeproj//xcodeproj:extensions.bzl", + "extensionName": "internal", + "usingModule": "rules_xcodeproj@_", + "location": { + "file": "@@rules_xcodeproj~//:MODULE.bazel", + "line": 23, + "column": 25 + }, + "imports": { + "rules_xcodeproj_generated": "rules_xcodeproj_generated" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + }, + { + "extensionBzlFile": "@rules_xcodeproj//xcodeproj:extensions.bzl", + "extensionName": "non_module_deps", + "usingModule": "rules_xcodeproj@_", + "location": { + "file": "@@rules_xcodeproj~//:MODULE.bazel", + "line": 26, + "column": 32 + }, + "imports": { + "rules_xcodeproj_index_import": "rules_xcodeproj_index_import", + "com_github_apple_swift_argument_parser": "com_github_apple_swift_argument_parser", + "com_github_apple_swift_collections": "com_github_apple_swift_collections", + "com_github_kylef_pathkit": "com_github_kylef_pathkit", + "com_github_michaeleisel_jjliso8601dateformatter": "com_github_michaeleisel_jjliso8601dateformatter", + "com_github_michaeleisel_zippyjson": "com_github_michaeleisel_zippyjson", + "com_github_michaeleisel_zippyjsoncfamily": "com_github_michaeleisel_zippyjsoncfamily", + "com_github_tadija_aexml": "com_github_tadija_aexml", + "com_github_tuist_xcodeproj": "com_github_tuist_xcodeproj" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + } + ], "deps": { + "bazel_features": "bazel_features@1.9.1", + "bazel_skylib": "bazel_skylib@1.5.0", + "build_bazel_rules_swift": "rules_swift@_", + "build_bazel_rules_apple": "rules_apple@_", + "rules_python": "rules_python@0.27.1", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" } @@ -46,7 +410,7 @@ "usingModule": "bazel_tools@_", "location": { "file": "@@bazel_tools//:MODULE.bazel", - "line": 17, + "line": 18, "column": 29 }, "imports": { @@ -64,7 +428,7 @@ "usingModule": "bazel_tools@_", "location": { "file": "@@bazel_tools//:MODULE.bazel", - "line": 21, + "line": 22, "column": 32 }, "imports": { @@ -81,7 +445,7 @@ "usingModule": "bazel_tools@_", "location": { "file": "@@bazel_tools//:MODULE.bazel", - "line": 24, + "line": 25, "column": 32 }, "imports": { @@ -103,7 +467,7 @@ "usingModule": "bazel_tools@_", "location": { "file": "@@bazel_tools//:MODULE.bazel", - "line": 35, + "line": 36, "column": 39 }, "imports": { @@ -120,7 +484,7 @@ "usingModule": "bazel_tools@_", "location": { "file": "@@bazel_tools//:MODULE.bazel", - "line": 39, + "line": 40, "column": 48 }, "imports": { @@ -137,7 +501,7 @@ "usingModule": "bazel_tools@_", "location": { "file": "@@bazel_tools//:MODULE.bazel", - "line": 42, + "line": 43, "column": 42 }, "imports": { @@ -148,18 +512,36 @@ "tags": [], "hasDevUseExtension": false, "hasNonDevUseExtension": true + }, + { + "extensionBzlFile": "@buildozer//:buildozer_binary.bzl", + "extensionName": "buildozer_binary", + "usingModule": "bazel_tools@_", + "location": { + "file": "@@bazel_tools//:MODULE.bazel", + "line": 47, + "column": 33 + }, + "imports": { + "buildozer_binary": "buildozer_binary" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true } ], "deps": { "rules_cc": "rules_cc@0.0.9", - "rules_java": "rules_java@7.1.0", + "rules_java": "rules_java@7.4.0", "rules_license": "rules_license@0.0.7", - "rules_proto": "rules_proto@4.0.0", - "rules_python": "rules_python@0.4.0", - "platforms": "platforms@0.0.7", - "com_google_protobuf": "protobuf@3.19.6", + "rules_proto": "rules_proto@5.3.0-21.7", + "rules_python": "rules_python@0.27.1", + "buildozer": "buildozer@6.4.0.2", + "platforms": "platforms@0.0.9", + "com_google_protobuf": "protobuf@21.7", "zlib": "zlib@1.3", - "build_bazel_apple_support": "apple_support@1.5.0", + "build_bazel_apple_support": "apple_support@_", "local_config_platform": "local_config_platform@_" } }, @@ -172,394 +554,801 @@ "toolchainsToRegister": [], "extensionUsages": [], "deps": { - "platforms": "platforms@0.0.7", + "platforms": "platforms@0.0.9", "bazel_tools": "bazel_tools@_" } }, - "rules_cc@0.0.9": { - "name": "rules_cc", - "version": "0.0.9", - "key": "rules_cc@0.0.9", - "repoName": "rules_cc", + "cgrindel_bazel_starlib@0.21.0": { + "name": "cgrindel_bazel_starlib", + "version": "0.21.0", + "key": "cgrindel_bazel_starlib@0.21.0", + "repoName": "cgrindel_bazel_starlib", "executionPlatformsToRegister": [], - "toolchainsToRegister": [ - "@local_config_cc_toolchains//:all" - ], + "toolchainsToRegister": [], "extensionUsages": [ { - "extensionBzlFile": "@bazel_tools//tools/cpp:cc_configure.bzl", - "extensionName": "cc_configure_extension", - "usingModule": "rules_cc@0.0.9", + "extensionBzlFile": "@bazel_gazelle//:extensions.bzl", + "extensionName": "go_deps", + "usingModule": "cgrindel_bazel_starlib@0.21.0", "location": { - "file": "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel", - "line": 9, - "column": 29 + "file": "https://bcr.bazel.build/modules/cgrindel_bazel_starlib/0.21.0/MODULE.bazel", + "line": 31, + "column": 24 }, "imports": { - "local_config_cc_toolchains": "local_config_cc_toolchains" + "com_github_creasty_defaults": "com_github_creasty_defaults", + "com_github_gomarkdown_markdown": "com_github_gomarkdown_markdown", + "com_github_stretchr_testify": "com_github_stretchr_testify", + "in_gopkg_yaml_v3": "in_gopkg_yaml_v3" }, "devImports": [], - "tags": [], + "tags": [ + { + "tagName": "from_file", + "attributeValues": { + "go_mod": "//:go.mod" + }, + "devDependency": false, + "location": { + "file": "https://bcr.bazel.build/modules/cgrindel_bazel_starlib/0.21.0/MODULE.bazel", + "line": 32, + "column": 18 + } + } + ], "hasDevUseExtension": false, "hasNonDevUseExtension": true } ], "deps": { - "platforms": "platforms@0.0.7", + "io_bazel_rules_go": "rules_go@0.47.0", + "bazel_gazelle": "gazelle@0.37.0", + "bazel_skylib": "bazel_skylib@1.5.0", + "io_bazel_stardoc": "stardoc@0.6.2", + "buildifier_prebuilt": "buildifier_prebuilt@6.0.0.1", + "platforms": "platforms@0.0.9", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" }, "repoSpec": { - "bzlFile": "@bazel_tools//tools/build_defs/repo:http.bzl", + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", "ruleClassName": "http_archive", "attributes": { - "name": "rules_cc~0.0.9", "urls": [ - "https://github.com/bazelbuild/rules_cc/releases/download/0.0.9/rules_cc-0.0.9.tar.gz" + "https://github.com/cgrindel/bazel-starlib/releases/download/v0.21.0/bazel-starlib.v0.21.0.tar.gz" ], - "integrity": "sha256-IDeHW5pEVtzkp50RKorohbvEqtlo5lh9ym5k86CQDN8=", - "strip_prefix": "rules_cc-0.0.9", + "integrity": "sha256-Q+N1IT2r4MOSjmVBLqfsFoUNuTKFyMb4sOqkHKzQ+II=", + "strip_prefix": "", "remote_patches": { - "https://bcr.bazel.build/modules/rules_cc/0.0.9/patches/module_dot_bazel_version.patch": "sha256-mM+qzOI0SgAdaJBlWOSMwMPKpaA9b7R37Hj/tp5bb4g=" + "https://bcr.bazel.build/modules/cgrindel_bazel_starlib/0.21.0/patches/module_dot_bazel_version.patch": "sha256-hKDFxjpoeavbJZK7KL182NpKl3Mm2UU89KFdgKt84eI=" }, - "remote_patch_strip": 0 + "remote_patch_strip": 1 } } }, - "rules_java@7.1.0": { - "name": "rules_java", - "version": "7.1.0", - "key": "rules_java@7.1.0", - "repoName": "rules_java", + "bazel_skylib@1.5.0": { + "name": "bazel_skylib", + "version": "1.5.0", + "key": "bazel_skylib@1.5.0", + "repoName": "bazel_skylib", "executionPlatformsToRegister": [], "toolchainsToRegister": [ - "//toolchains:all", - "@local_jdk//:runtime_toolchain_definition", - "@local_jdk//:bootstrap_runtime_toolchain_definition", - "@remotejdk11_linux_toolchain_config_repo//:all", - "@remotejdk11_linux_aarch64_toolchain_config_repo//:all", - "@remotejdk11_linux_ppc64le_toolchain_config_repo//:all", - "@remotejdk11_linux_s390x_toolchain_config_repo//:all", - "@remotejdk11_macos_toolchain_config_repo//:all", - "@remotejdk11_macos_aarch64_toolchain_config_repo//:all", - "@remotejdk11_win_toolchain_config_repo//:all", - "@remotejdk11_win_arm64_toolchain_config_repo//:all", - "@remotejdk17_linux_toolchain_config_repo//:all", - "@remotejdk17_linux_aarch64_toolchain_config_repo//:all", - "@remotejdk17_linux_ppc64le_toolchain_config_repo//:all", - "@remotejdk17_linux_s390x_toolchain_config_repo//:all", - "@remotejdk17_macos_toolchain_config_repo//:all", - "@remotejdk17_macos_aarch64_toolchain_config_repo//:all", - "@remotejdk17_win_toolchain_config_repo//:all", - "@remotejdk17_win_arm64_toolchain_config_repo//:all", - "@remotejdk21_linux_toolchain_config_repo//:all", - "@remotejdk21_linux_aarch64_toolchain_config_repo//:all", - "@remotejdk21_macos_toolchain_config_repo//:all", - "@remotejdk21_macos_aarch64_toolchain_config_repo//:all", - "@remotejdk21_win_toolchain_config_repo//:all" + "//toolchains/unittest:cmd_toolchain", + "//toolchains/unittest:bash_toolchain" + ], + "extensionUsages": [], + "deps": { + "platforms": "platforms@0.0.9", + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + }, + "repoSpec": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz" + ], + "integrity": "sha256-zVWgYudjuTSZIfD124w5MyiNyLpPdt2UFqrGis7jy5Q=", + "strip_prefix": "", + "remote_patches": {}, + "remote_patch_strip": 0 + } + } + }, + "rules_go@0.47.0": { + "name": "rules_go", + "version": "0.47.0", + "key": "rules_go@0.47.0", + "repoName": "io_bazel_rules_go", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [ + "@go_toolchains//:all" ], "extensionUsages": [ { - "extensionBzlFile": "@rules_java//java:extensions.bzl", - "extensionName": "toolchains", - "usingModule": "rules_java@7.1.0", + "extensionBzlFile": "@io_bazel_rules_go//go:extensions.bzl", + "extensionName": "go_sdk", + "usingModule": "rules_go@0.47.0", "location": { - "file": "https://bcr.bazel.build/modules/rules_java/7.1.0/MODULE.bazel", - "line": 19, - "column": 27 + "file": "https://bcr.bazel.build/modules/rules_go/0.47.0/MODULE.bazel", + "line": 16, + "column": 23 }, "imports": { - "remote_java_tools": "remote_java_tools", - "remote_java_tools_linux": "remote_java_tools_linux", - "remote_java_tools_windows": "remote_java_tools_windows", - "remote_java_tools_darwin_x86_64": "remote_java_tools_darwin_x86_64", - "remote_java_tools_darwin_arm64": "remote_java_tools_darwin_arm64", - "local_jdk": "local_jdk", - "remotejdk11_linux_toolchain_config_repo": "remotejdk11_linux_toolchain_config_repo", - "remotejdk11_linux_aarch64_toolchain_config_repo": "remotejdk11_linux_aarch64_toolchain_config_repo", - "remotejdk11_linux_ppc64le_toolchain_config_repo": "remotejdk11_linux_ppc64le_toolchain_config_repo", - "remotejdk11_linux_s390x_toolchain_config_repo": "remotejdk11_linux_s390x_toolchain_config_repo", - "remotejdk11_macos_toolchain_config_repo": "remotejdk11_macos_toolchain_config_repo", - "remotejdk11_macos_aarch64_toolchain_config_repo": "remotejdk11_macos_aarch64_toolchain_config_repo", - "remotejdk11_win_toolchain_config_repo": "remotejdk11_win_toolchain_config_repo", - "remotejdk11_win_arm64_toolchain_config_repo": "remotejdk11_win_arm64_toolchain_config_repo", - "remotejdk17_linux_toolchain_config_repo": "remotejdk17_linux_toolchain_config_repo", - "remotejdk17_linux_aarch64_toolchain_config_repo": "remotejdk17_linux_aarch64_toolchain_config_repo", - "remotejdk17_linux_ppc64le_toolchain_config_repo": "remotejdk17_linux_ppc64le_toolchain_config_repo", - "remotejdk17_linux_s390x_toolchain_config_repo": "remotejdk17_linux_s390x_toolchain_config_repo", - "remotejdk17_macos_toolchain_config_repo": "remotejdk17_macos_toolchain_config_repo", - "remotejdk17_macos_aarch64_toolchain_config_repo": "remotejdk17_macos_aarch64_toolchain_config_repo", - "remotejdk17_win_toolchain_config_repo": "remotejdk17_win_toolchain_config_repo", - "remotejdk17_win_arm64_toolchain_config_repo": "remotejdk17_win_arm64_toolchain_config_repo", - "remotejdk21_linux_toolchain_config_repo": "remotejdk21_linux_toolchain_config_repo", - "remotejdk21_linux_aarch64_toolchain_config_repo": "remotejdk21_linux_aarch64_toolchain_config_repo", - "remotejdk21_macos_toolchain_config_repo": "remotejdk21_macos_toolchain_config_repo", - "remotejdk21_macos_aarch64_toolchain_config_repo": "remotejdk21_macos_aarch64_toolchain_config_repo", - "remotejdk21_win_toolchain_config_repo": "remotejdk21_win_toolchain_config_repo" + "go_toolchains": "go_toolchains", + "io_bazel_rules_nogo": "io_bazel_rules_nogo" }, "devImports": [], - "tags": [], + "tags": [ + { + "tagName": "download", + "attributeValues": { + "name": "go_default_sdk", + "version": "1.21.8" + }, + "devDependency": false, + "location": { + "file": "https://bcr.bazel.build/modules/rules_go/0.47.0/MODULE.bazel", + "line": 17, + "column": 16 + } + } + ], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + }, + { + "extensionBzlFile": "@gazelle//:extensions.bzl", + "extensionName": "go_deps", + "usingModule": "rules_go@0.47.0", + "location": { + "file": "https://bcr.bazel.build/modules/rules_go/0.47.0/MODULE.bazel", + "line": 32, + "column": 24 + }, + "imports": { + "com_github_gogo_protobuf": "com_github_gogo_protobuf", + "com_github_golang_mock": "com_github_golang_mock", + "com_github_golang_protobuf": "com_github_golang_protobuf", + "org_golang_google_genproto": "org_golang_google_genproto", + "org_golang_google_grpc": "org_golang_google_grpc", + "org_golang_google_grpc_cmd_protoc_gen_go_grpc": "org_golang_google_grpc_cmd_protoc_gen_go_grpc", + "org_golang_google_protobuf": "org_golang_google_protobuf", + "org_golang_x_net": "org_golang_x_net", + "org_golang_x_tools": "org_golang_x_tools", + "bazel_gazelle_go_repository_config": "bazel_gazelle_go_repository_config" + }, + "devImports": [], + "tags": [ + { + "tagName": "from_file", + "attributeValues": { + "go_mod": "//:go.mod" + }, + "devDependency": false, + "location": { + "file": "https://bcr.bazel.build/modules/rules_go/0.47.0/MODULE.bazel", + "line": 33, + "column": 18 + } + } + ], "hasDevUseExtension": false, "hasNonDevUseExtension": true } ], "deps": { - "platforms": "platforms@0.0.7", - "rules_cc": "rules_cc@0.0.9", - "bazel_skylib": "bazel_skylib@1.3.0", - "rules_proto": "rules_proto@4.0.0", - "rules_license": "rules_license@0.0.7", + "io_bazel_rules_go_bazel_features": "bazel_features@1.9.1", + "bazel_skylib": "bazel_skylib@1.5.0", + "platforms": "platforms@0.0.9", + "rules_proto": "rules_proto@5.3.0-21.7", + "com_google_protobuf": "protobuf@21.7", + "gazelle": "gazelle@0.37.0", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" }, "repoSpec": { - "bzlFile": "@bazel_tools//tools/build_defs/repo:http.bzl", + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", "ruleClassName": "http_archive", "attributes": { - "name": "rules_java~7.1.0", "urls": [ - "https://github.com/bazelbuild/rules_java/releases/download/7.1.0/rules_java-7.1.0.tar.gz" + "https://github.com/bazelbuild/rules_go/releases/download/v0.47.0/rules_go-v0.47.0.zip" ], - "integrity": "sha256-o3pOX2OrgnFuXdau75iO2EYcegC46TYnImKJn1h81OE=", + "integrity": "sha256-r0fzDpy9cK405Jhm4gGz93Bpq7ERGD8sApfn50umu8A=", "strip_prefix": "", "remote_patches": {}, "remote_patch_strip": 0 } } }, - "rules_license@0.0.7": { - "name": "rules_license", - "version": "0.0.7", - "key": "rules_license@0.0.7", - "repoName": "rules_license", + "rules_cc@0.0.9": { + "name": "rules_cc", + "version": "0.0.9", + "key": "rules_cc@0.0.9", + "repoName": "rules_cc", "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [], + "toolchainsToRegister": [ + "@local_config_cc_toolchains//:all" + ], + "extensionUsages": [ + { + "extensionBzlFile": "@bazel_tools//tools/cpp:cc_configure.bzl", + "extensionName": "cc_configure_extension", + "usingModule": "rules_cc@0.0.9", + "location": { + "file": "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel", + "line": 9, + "column": 29 + }, + "imports": { + "local_config_cc_toolchains": "local_config_cc_toolchains" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + } + ], "deps": { + "platforms": "platforms@0.0.9", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" }, "repoSpec": { - "bzlFile": "@bazel_tools//tools/build_defs/repo:http.bzl", + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", "ruleClassName": "http_archive", "attributes": { - "name": "rules_license~0.0.7", "urls": [ - "https://github.com/bazelbuild/rules_license/releases/download/0.0.7/rules_license-0.0.7.tar.gz" + "https://github.com/bazelbuild/rules_cc/releases/download/0.0.9/rules_cc-0.0.9.tar.gz" ], - "integrity": "sha256-RTHezLkTY5ww5cdRKgVNXYdWmNrrddjPkPKEN1/nw2A=", - "strip_prefix": "", - "remote_patches": {}, + "integrity": "sha256-IDeHW5pEVtzkp50RKorohbvEqtlo5lh9ym5k86CQDN8=", + "strip_prefix": "rules_cc-0.0.9", + "remote_patches": { + "https://bcr.bazel.build/modules/rules_cc/0.0.9/patches/module_dot_bazel_version.patch": "sha256-mM+qzOI0SgAdaJBlWOSMwMPKpaA9b7R37Hj/tp5bb4g=" + }, "remote_patch_strip": 0 } } }, - "rules_proto@4.0.0": { - "name": "rules_proto", - "version": "4.0.0", - "key": "rules_proto@4.0.0", - "repoName": "rules_proto", + "platforms@0.0.9": { + "name": "platforms", + "version": "0.0.9", + "key": "platforms@0.0.9", + "repoName": "platforms", "executionPlatformsToRegister": [], "toolchainsToRegister": [], - "extensionUsages": [], + "extensionUsages": [ + { + "extensionBzlFile": "@platforms//host:extension.bzl", + "extensionName": "host_platform", + "usingModule": "platforms@0.0.9", + "location": { + "file": "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel", + "line": 9, + "column": 30 + }, + "imports": { + "host_platform": "host_platform" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + } + ], "deps": { - "bazel_skylib": "bazel_skylib@1.3.0", - "rules_cc": "rules_cc@0.0.9", + "rules_license": "rules_license@0.0.7", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" }, "repoSpec": { - "bzlFile": "@bazel_tools//tools/build_defs/repo:http.bzl", + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", "ruleClassName": "http_archive", "attributes": { - "name": "rules_proto~4.0.0", "urls": [ - "https://github.com/bazelbuild/rules_proto/archive/refs/tags/4.0.0.zip" + "https://github.com/bazelbuild/platforms/releases/download/0.0.9/platforms-0.0.9.tar.gz" ], - "integrity": "sha256-Lr5z6xyuRA19pNtRYMGjKaynwQpck4H/lwYyVjyhoq4=", - "strip_prefix": "rules_proto-4.0.0", - "remote_patches": { - "https://bcr.bazel.build/modules/rules_proto/4.0.0/patches/module_dot_bazel.patch": "sha256-MclJO7tIAM2ElDAmscNId9pKTpOuDGHgVlW/9VBOIp0=" - }, + "integrity": "sha256-XtpTnIQSZQMcL4LYrno6ZJC9YhduDAOPxGnqv5H2FJs=", + "strip_prefix": "", + "remote_patches": {}, "remote_patch_strip": 0 } } }, - "rules_python@0.4.0": { - "name": "rules_python", - "version": "0.4.0", - "key": "rules_python@0.4.0", - "repoName": "rules_python", + "gazelle@0.37.0": { + "name": "gazelle", + "version": "0.37.0", + "key": "gazelle@0.37.0", + "repoName": "bazel_gazelle", "executionPlatformsToRegister": [], - "toolchainsToRegister": [ - "@bazel_tools//tools/python:autodetecting_toolchain" - ], + "toolchainsToRegister": [], "extensionUsages": [ { - "extensionBzlFile": "@rules_python//bzlmod:extensions.bzl", - "extensionName": "pip_install", - "usingModule": "rules_python@0.4.0", + "extensionBzlFile": "@io_bazel_rules_go//go:extensions.bzl", + "extensionName": "go_sdk", + "usingModule": "gazelle@0.37.0", "location": { - "file": "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel", - "line": 7, - "column": 28 + "file": "https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel", + "line": 13, + "column": 23 }, "imports": { - "pypi__click": "pypi__click", - "pypi__pip": "pypi__pip", - "pypi__pip_tools": "pypi__pip_tools", - "pypi__pkginfo": "pypi__pkginfo", - "pypi__setuptools": "pypi__setuptools", - "pypi__wheel": "pypi__wheel" + "go_host_compatible_sdk_label": "go_host_compatible_sdk_label" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + }, + { + "extensionBzlFile": "@bazel_gazelle//internal/bzlmod:non_module_deps.bzl", + "extensionName": "non_module_deps", + "usingModule": "gazelle@0.37.0", + "location": { + "file": "https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel", + "line": 21, + "column": 32 + }, + "imports": { + "bazel_gazelle_go_repository_cache": "bazel_gazelle_go_repository_cache", + "bazel_gazelle_go_repository_tools": "bazel_gazelle_go_repository_tools", + "bazel_gazelle_is_bazel_module": "bazel_gazelle_is_bazel_module" }, "devImports": [], "tags": [], "hasDevUseExtension": false, "hasNonDevUseExtension": true + }, + { + "extensionBzlFile": "@bazel_gazelle//:extensions.bzl", + "extensionName": "go_deps", + "usingModule": "gazelle@0.37.0", + "location": { + "file": "https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel", + "line": 29, + "column": 24 + }, + "imports": { + "com_github_bazelbuild_buildtools": "com_github_bazelbuild_buildtools", + "com_github_bmatcuk_doublestar_v4": "com_github_bmatcuk_doublestar_v4", + "com_github_fsnotify_fsnotify": "com_github_fsnotify_fsnotify", + "com_github_google_go_cmp": "com_github_google_go_cmp", + "com_github_pmezard_go_difflib": "com_github_pmezard_go_difflib", + "org_golang_x_mod": "org_golang_x_mod", + "org_golang_x_sync": "org_golang_x_sync", + "org_golang_x_tools": "org_golang_x_tools", + "org_golang_x_tools_go_vcs": "org_golang_x_tools_go_vcs", + "bazel_gazelle_go_repository_config": "bazel_gazelle_go_repository_config", + "com_github_golang_protobuf": "com_github_golang_protobuf", + "org_golang_google_protobuf": "org_golang_google_protobuf" + }, + "devImports": [], + "tags": [ + { + "tagName": "from_file", + "attributeValues": { + "go_mod": "//:go.mod" + }, + "devDependency": false, + "location": { + "file": "https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel", + "line": 30, + "column": 18 + } + }, + { + "tagName": "module", + "attributeValues": { + "path": "golang.org/x/tools", + "sum": "h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=", + "version": "v0.18.0" + }, + "devDependency": false, + "location": { + "file": "https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel", + "line": 34, + "column": 15 + } + } + ], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true } ], "deps": { + "bazel_features": "bazel_features@1.9.1", + "bazel_skylib": "bazel_skylib@1.5.0", + "com_google_protobuf": "protobuf@21.7", + "io_bazel_rules_go": "rules_go@0.47.0", + "rules_proto": "rules_proto@5.3.0-21.7", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" }, "repoSpec": { - "bzlFile": "@bazel_tools//tools/build_defs/repo:http.bzl", + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", "ruleClassName": "http_archive", "attributes": { - "name": "rules_python~0.4.0", "urls": [ - "https://github.com/bazelbuild/rules_python/releases/download/0.4.0/rules_python-0.4.0.tar.gz" + "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.37.0/bazel-gazelle-v0.37.0.tar.gz" ], - "integrity": "sha256-lUqom0kb5KCDMEosuDgBnIuMNyCnq7nEy4GseiQjDOo=", + "integrity": "sha256-12v3pg/YsFBEQJDfooN6Tq+YKeEWVhjuNdzspcvfWNU=", "strip_prefix": "", - "remote_patches": { - "https://bcr.bazel.build/modules/rules_python/0.4.0/patches/propagate_pip_install_dependencies.patch": "sha256-v7S/dem/mixg63MF4KoRGDA4KEol9ab/tIVp+6Xq0D0=", - "https://bcr.bazel.build/modules/rules_python/0.4.0/patches/module_dot_bazel.patch": "sha256-kG4VIfWxQazzTuh50mvsx6pmyoRVA4lfH5rkto/Oq+Y=" - }, - "remote_patch_strip": 1 + "remote_patches": {}, + "remote_patch_strip": 0 } } }, - "platforms@0.0.7": { - "name": "platforms", - "version": "0.0.7", - "key": "platforms@0.0.7", - "repoName": "platforms", + "bazel_features@1.9.1": { + "name": "bazel_features", + "version": "1.9.1", + "key": "bazel_features@1.9.1", + "repoName": "bazel_features", "executionPlatformsToRegister": [], "toolchainsToRegister": [], - "extensionUsages": [], + "extensionUsages": [ + { + "extensionBzlFile": "@bazel_features//private:extensions.bzl", + "extensionName": "version_extension", + "usingModule": "bazel_features@1.9.1", + "location": { + "file": "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel", + "line": 15, + "column": 24 + }, + "imports": { + "bazel_features_globals": "bazel_features_globals", + "bazel_features_version": "bazel_features_version" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + } + ], "deps": { - "rules_license": "rules_license@0.0.7", + "bazel_skylib": "bazel_skylib@1.5.0", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" }, "repoSpec": { - "bzlFile": "@bazel_tools//tools/build_defs/repo:http.bzl", + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", "ruleClassName": "http_archive", "attributes": { - "name": "platforms", "urls": [ - "https://github.com/bazelbuild/platforms/releases/download/0.0.7/platforms-0.0.7.tar.gz" + "https://github.com/bazel-contrib/bazel_features/releases/download/v1.9.1/bazel_features-v1.9.1.tar.gz" ], - "integrity": "sha256-OlYcmee9vpFzqmU/1Xn+hJ8djWc5V4CrR3Cx84FDHVE=", - "strip_prefix": "", - "remote_patches": {}, - "remote_patch_strip": 0 + "integrity": "sha256-13h9oomn+0lzUiEa0gDsn2mIIqngdXpJdv2fcT/zcrM=", + "strip_prefix": "bazel_features-1.9.1", + "remote_patches": { + "https://bcr.bazel.build/modules/bazel_features/1.9.1/patches/module_dot_bazel_version.patch": "sha256-a2ofwS5r2Qq+WxzVa7sLbRXhfT3JoYxSlUVQH/nL454=" + }, + "remote_patch_strip": 1 } } }, - "protobuf@3.19.6": { + "protobuf@21.7": { "name": "protobuf", - "version": "3.19.6", - "key": "protobuf@3.19.6", + "version": "21.7", + "key": "protobuf@21.7", "repoName": "protobuf", "executionPlatformsToRegister": [], "toolchainsToRegister": [], - "extensionUsages": [], + "extensionUsages": [ + { + "extensionBzlFile": "@rules_jvm_external//:extensions.bzl", + "extensionName": "maven", + "usingModule": "protobuf@21.7", + "location": { + "file": "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel", + "line": 22, + "column": 22 + }, + "imports": { + "maven": "maven" + }, + "devImports": [], + "tags": [ + { + "tagName": "install", + "attributeValues": { + "name": "maven", + "artifacts": [ + "com.google.code.findbugs:jsr305:3.0.2", + "com.google.code.gson:gson:2.8.9", + "com.google.errorprone:error_prone_annotations:2.3.2", + "com.google.j2objc:j2objc-annotations:1.3", + "com.google.guava:guava:31.1-jre", + "com.google.guava:guava-testlib:31.1-jre", + "com.google.truth:truth:1.1.2", + "junit:junit:4.13.2", + "org.mockito:mockito-core:4.3.1" + ] + }, + "devDependency": false, + "location": { + "file": "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel", + "line": 24, + "column": 14 + } + } + ], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + } + ], "deps": { - "bazel_skylib": "bazel_skylib@1.3.0", - "zlib": "zlib@1.3", - "rules_python": "rules_python@0.4.0", + "bazel_skylib": "bazel_skylib@1.5.0", + "rules_python": "rules_python@0.27.1", "rules_cc": "rules_cc@0.0.9", - "rules_proto": "rules_proto@4.0.0", - "rules_java": "rules_java@7.1.0", + "rules_proto": "rules_proto@5.3.0-21.7", + "rules_java": "rules_java@7.4.0", + "rules_pkg": "rules_pkg@0.7.0", + "com_google_abseil": "abseil-cpp@20211102.0", + "zlib": "zlib@1.3", + "upb": "upb@0.0.0-20220923-a547704", + "rules_jvm_external": "rules_jvm_external@5.2", + "com_google_googletest": "googletest@1.11.0", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" }, "repoSpec": { - "bzlFile": "@bazel_tools//tools/build_defs/repo:http.bzl", + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", "ruleClassName": "http_archive", "attributes": { - "name": "protobuf~3.19.6", "urls": [ - "https://github.com/protocolbuffers/protobuf/archive/refs/tags/v3.19.6.zip" + "https://github.com/protocolbuffers/protobuf/releases/download/v21.7/protobuf-all-21.7.zip" ], - "integrity": "sha256-OH4sVZuyx8G8N5jE5s/wFTgaebJ1hpavy/johzC0c4k=", - "strip_prefix": "protobuf-3.19.6", + "integrity": "sha256-VJOiH17T/FAuZv7GuUScBqVRztYwAvpIkDxA36jeeko=", + "strip_prefix": "protobuf-21.7", "remote_patches": { - "https://bcr.bazel.build/modules/protobuf/3.19.6/patches/relative_repo_names.patch": "sha256-w/5gw/zGv8NFId+669hcdw1Uus2lxgYpulATHIwIByI=", - "https://bcr.bazel.build/modules/protobuf/3.19.6/patches/remove_dependency_on_rules_jvm_external.patch": "sha256-THUTnVgEBmjA0W7fKzIyZOVG58DnW9HQTkr4D2zKUUc=", - "https://bcr.bazel.build/modules/protobuf/3.19.6/patches/add_module_dot_bazel_for_examples.patch": "sha256-s/b1gi3baK3LsXefI2rQilhmkb2R5jVJdnT6zEcdfHY=", - "https://bcr.bazel.build/modules/protobuf/3.19.6/patches/module_dot_bazel.patch": "sha256-S0DEni8zgx7rHscW3z/rCEubQnYec0XhNet640cw0h4=" + "https://bcr.bazel.build/modules/protobuf/21.7/patches/add_module_dot_bazel.patch": "sha256-q3V2+eq0v2XF0z8z+V+QF4cynD6JvHI1y3kI/+rzl5s=", + "https://bcr.bazel.build/modules/protobuf/21.7/patches/add_module_dot_bazel_for_examples.patch": "sha256-O7YP6s3lo/1opUiO0jqXYORNHdZ/2q3hjz1QGy8QdIU=", + "https://bcr.bazel.build/modules/protobuf/21.7/patches/relative_repo_names.patch": "sha256-RK9RjW8T5UJNG7flIrnFiNE9vKwWB+8uWWtJqXYT0w4=", + "https://bcr.bazel.build/modules/protobuf/21.7/patches/add_missing_files.patch": "sha256-Hyne4DG2u5bXcWHNxNMirA2QFAe/2Cl8oMm1XJdkQIY=" }, "remote_patch_strip": 1 } } }, - "zlib@1.3": { - "name": "zlib", - "version": "1.3", - "key": "zlib@1.3", - "repoName": "zlib", + "rules_proto@5.3.0-21.7": { + "name": "rules_proto", + "version": "5.3.0-21.7", + "key": "rules_proto@5.3.0-21.7", + "repoName": "rules_proto", "executionPlatformsToRegister": [], "toolchainsToRegister": [], "extensionUsages": [], "deps": { - "platforms": "platforms@0.0.7", + "bazel_skylib": "bazel_skylib@1.5.0", + "com_google_protobuf": "protobuf@21.7", "rules_cc": "rules_cc@0.0.9", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" }, "repoSpec": { - "bzlFile": "@bazel_tools//tools/build_defs/repo:http.bzl", + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", "ruleClassName": "http_archive", "attributes": { - "name": "zlib~1.3", "urls": [ - "https://github.com/madler/zlib/releases/download/v1.3/zlib-1.3.tar.gz" + "https://github.com/bazelbuild/rules_proto/archive/refs/tags/5.3.0-21.7.tar.gz" ], - "integrity": "sha256-/wukwpIBPbwnUws6geH5qBPNOd4Byl4Pi/NVcC76WT4=", - "strip_prefix": "zlib-1.3", + "integrity": "sha256-3D+yBqLLNEG0heseQjFlsjEjWh6psDG0Qzz3vB+kYN0=", + "strip_prefix": "rules_proto-5.3.0-21.7", + "remote_patches": {}, + "remote_patch_strip": 0 + } + } + }, + "nlohmann_json@3.6.1": { + "name": "nlohmann_json", + "version": "3.6.1", + "key": "nlohmann_json@3.6.1", + "repoName": "nlohmann_json", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [], + "extensionUsages": [], + "deps": { + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + }, + "repoSpec": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/nlohmann/json/releases/download/v3.6.1/include.zip" + ], + "integrity": "sha256-acyIIHzpE0fqUwsif/B3bbgty43mcE4aPXT0hBvGUc8=", + "strip_prefix": "", "remote_patches": { - "https://bcr.bazel.build/modules/zlib/1.3/patches/add_build_file.patch": "sha256-Ei+FYaaOo7A3jTKunMEodTI0Uw5NXQyZEcboMC8JskY=", - "https://bcr.bazel.build/modules/zlib/1.3/patches/module_dot_bazel.patch": "sha256-fPWLM+2xaF/kuy+kZc1YTfW6hNjrkG400Ho7gckuyJk=" + "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/patches/add_build_file.patch": "sha256-q7pmw7dn3H7Le3BgkydhrvZG+1e75JisnM+PLaPjCI0=", + "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/patches/module_dot_bazel.patch": "sha256-91o2FwT6kCVE+BBM+L2rxXFYbVDeDSkKHabs50GBa5o=" }, "remote_patch_strip": 0 } } }, - "apple_support@1.5.0": { - "name": "apple_support", - "version": "1.5.0", - "key": "apple_support@1.5.0", - "repoName": "build_bazel_apple_support", + "rules_python@0.27.1": { + "name": "rules_python", + "version": "0.27.1", + "key": "rules_python@0.27.1", + "repoName": "rules_python", "executionPlatformsToRegister": [], "toolchainsToRegister": [ - "@local_config_apple_cc_toolchains//:all" + "@pythons_hub//:all" ], "extensionUsages": [ { - "extensionBzlFile": "@build_bazel_apple_support//crosstool:setup.bzl", - "extensionName": "apple_cc_configure_extension", - "usingModule": "apple_support@1.5.0", + "extensionBzlFile": "@rules_python//python/private/bzlmod:internal_deps.bzl", + "extensionName": "internal_deps", + "usingModule": "rules_python@0.27.1", "location": { - "file": "https://bcr.bazel.build/modules/apple_support/1.5.0/MODULE.bazel", + "file": "https://bcr.bazel.build/modules/rules_python/0.27.1/MODULE.bazel", "line": 17, - "column": 35 + "column": 30 }, "imports": { - "local_config_apple_cc": "local_config_apple_cc", - "local_config_apple_cc_toolchains": "local_config_apple_cc_toolchains" + "rules_python_internal": "rules_python_internal", + "pypi__build": "pypi__build", + "pypi__click": "pypi__click", + "pypi__colorama": "pypi__colorama", + "pypi__importlib_metadata": "pypi__importlib_metadata", + "pypi__installer": "pypi__installer", + "pypi__more_itertools": "pypi__more_itertools", + "pypi__packaging": "pypi__packaging", + "pypi__pep517": "pypi__pep517", + "pypi__pip": "pypi__pip", + "pypi__pip_tools": "pypi__pip_tools", + "pypi__pyproject_hooks": "pypi__pyproject_hooks", + "pypi__setuptools": "pypi__setuptools", + "pypi__tomli": "pypi__tomli", + "pypi__wheel": "pypi__wheel", + "pypi__zipp": "pypi__zipp" + }, + "devImports": [], + "tags": [ + { + "tagName": "install", + "attributeValues": {}, + "devDependency": false, + "location": { + "file": "https://bcr.bazel.build/modules/rules_python/0.27.1/MODULE.bazel", + "line": 18, + "column": 22 + } + } + ], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + }, + { + "extensionBzlFile": "@rules_python//python/extensions:python.bzl", + "extensionName": "python", + "usingModule": "rules_python@0.27.1", + "location": { + "file": "https://bcr.bazel.build/modules/rules_python/0.27.1/MODULE.bazel", + "line": 43, + "column": 23 + }, + "imports": { + "pythons_hub": "pythons_hub" + }, + "devImports": [], + "tags": [ + { + "tagName": "toolchain", + "attributeValues": { + "is_default": true, + "python_version": "3.11" + }, + "devDependency": false, + "location": { + "file": "https://bcr.bazel.build/modules/rules_python/0.27.1/MODULE.bazel", + "line": 49, + "column": 17 + } + } + ], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + } + ], + "deps": { + "bazel_features": "bazel_features@1.9.1", + "bazel_skylib": "bazel_skylib@1.5.0", + "platforms": "platforms@0.0.9", + "rules_proto": "rules_proto@5.3.0-21.7", + "com_google_protobuf": "protobuf@21.7", + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + }, + "repoSpec": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/bazelbuild/rules_python/releases/download/0.27.1/rules_python-0.27.1.tar.gz" + ], + "integrity": "sha256-6FrjDeM2JaY+yn/ECpT+qEXmQYiOUvMra+6pHosbJ5M=", + "strip_prefix": "rules_python-0.27.1", + "remote_patches": { + "https://bcr.bazel.build/modules/rules_python/0.27.1/patches/module_dot_bazel_version.patch": "sha256-Ier7Gb4zhbS273tClCov24gNYdheo4FdegZwaHBrQy0=" + }, + "remote_patch_strip": 1 + } + } + }, + "rules_java@7.4.0": { + "name": "rules_java", + "version": "7.4.0", + "key": "rules_java@7.4.0", + "repoName": "rules_java", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [ + "//toolchains:all", + "@local_jdk//:runtime_toolchain_definition", + "@local_jdk//:bootstrap_runtime_toolchain_definition", + "@remotejdk11_linux_toolchain_config_repo//:all", + "@remotejdk11_linux_aarch64_toolchain_config_repo//:all", + "@remotejdk11_linux_ppc64le_toolchain_config_repo//:all", + "@remotejdk11_linux_s390x_toolchain_config_repo//:all", + "@remotejdk11_macos_toolchain_config_repo//:all", + "@remotejdk11_macos_aarch64_toolchain_config_repo//:all", + "@remotejdk11_win_toolchain_config_repo//:all", + "@remotejdk11_win_arm64_toolchain_config_repo//:all", + "@remotejdk17_linux_toolchain_config_repo//:all", + "@remotejdk17_linux_aarch64_toolchain_config_repo//:all", + "@remotejdk17_linux_ppc64le_toolchain_config_repo//:all", + "@remotejdk17_linux_s390x_toolchain_config_repo//:all", + "@remotejdk17_macos_toolchain_config_repo//:all", + "@remotejdk17_macos_aarch64_toolchain_config_repo//:all", + "@remotejdk17_win_toolchain_config_repo//:all", + "@remotejdk17_win_arm64_toolchain_config_repo//:all", + "@remotejdk21_linux_toolchain_config_repo//:all", + "@remotejdk21_linux_aarch64_toolchain_config_repo//:all", + "@remotejdk21_macos_toolchain_config_repo//:all", + "@remotejdk21_macos_aarch64_toolchain_config_repo//:all", + "@remotejdk21_win_toolchain_config_repo//:all" + ], + "extensionUsages": [ + { + "extensionBzlFile": "@rules_java//java:extensions.bzl", + "extensionName": "toolchains", + "usingModule": "rules_java@7.4.0", + "location": { + "file": "https://bcr.bazel.build/modules/rules_java/7.4.0/MODULE.bazel", + "line": 19, + "column": 27 + }, + "imports": { + "remote_java_tools": "remote_java_tools", + "remote_java_tools_linux": "remote_java_tools_linux", + "remote_java_tools_windows": "remote_java_tools_windows", + "remote_java_tools_darwin_x86_64": "remote_java_tools_darwin_x86_64", + "remote_java_tools_darwin_arm64": "remote_java_tools_darwin_arm64", + "local_jdk": "local_jdk", + "remotejdk11_linux_toolchain_config_repo": "remotejdk11_linux_toolchain_config_repo", + "remotejdk11_linux_aarch64_toolchain_config_repo": "remotejdk11_linux_aarch64_toolchain_config_repo", + "remotejdk11_linux_ppc64le_toolchain_config_repo": "remotejdk11_linux_ppc64le_toolchain_config_repo", + "remotejdk11_linux_s390x_toolchain_config_repo": "remotejdk11_linux_s390x_toolchain_config_repo", + "remotejdk11_macos_toolchain_config_repo": "remotejdk11_macos_toolchain_config_repo", + "remotejdk11_macos_aarch64_toolchain_config_repo": "remotejdk11_macos_aarch64_toolchain_config_repo", + "remotejdk11_win_toolchain_config_repo": "remotejdk11_win_toolchain_config_repo", + "remotejdk11_win_arm64_toolchain_config_repo": "remotejdk11_win_arm64_toolchain_config_repo", + "remotejdk17_linux_toolchain_config_repo": "remotejdk17_linux_toolchain_config_repo", + "remotejdk17_linux_aarch64_toolchain_config_repo": "remotejdk17_linux_aarch64_toolchain_config_repo", + "remotejdk17_linux_ppc64le_toolchain_config_repo": "remotejdk17_linux_ppc64le_toolchain_config_repo", + "remotejdk17_linux_s390x_toolchain_config_repo": "remotejdk17_linux_s390x_toolchain_config_repo", + "remotejdk17_macos_toolchain_config_repo": "remotejdk17_macos_toolchain_config_repo", + "remotejdk17_macos_aarch64_toolchain_config_repo": "remotejdk17_macos_aarch64_toolchain_config_repo", + "remotejdk17_win_toolchain_config_repo": "remotejdk17_win_toolchain_config_repo", + "remotejdk17_win_arm64_toolchain_config_repo": "remotejdk17_win_arm64_toolchain_config_repo", + "remotejdk21_linux_toolchain_config_repo": "remotejdk21_linux_toolchain_config_repo", + "remotejdk21_linux_aarch64_toolchain_config_repo": "remotejdk21_linux_aarch64_toolchain_config_repo", + "remotejdk21_macos_toolchain_config_repo": "remotejdk21_macos_toolchain_config_repo", + "remotejdk21_macos_aarch64_toolchain_config_repo": "remotejdk21_macos_aarch64_toolchain_config_repo", + "remotejdk21_win_toolchain_config_repo": "remotejdk21_win_toolchain_config_repo" }, "devImports": [], "tags": [], @@ -568,57 +1357,2650 @@ } ], "deps": { - "bazel_skylib": "bazel_skylib@1.3.0", - "platforms": "platforms@0.0.7", + "platforms": "platforms@0.0.9", + "rules_cc": "rules_cc@0.0.9", + "bazel_skylib": "bazel_skylib@1.5.0", + "rules_proto": "rules_proto@5.3.0-21.7", + "rules_license": "rules_license@0.0.7", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" }, "repoSpec": { - "bzlFile": "@bazel_tools//tools/build_defs/repo:http.bzl", + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", "ruleClassName": "http_archive", "attributes": { - "name": "apple_support~1.5.0", "urls": [ - "https://github.com/bazelbuild/apple_support/releases/download/1.5.0/apple_support.1.5.0.tar.gz" + "https://github.com/bazelbuild/rules_java/releases/download/7.4.0/rules_java-7.4.0.tar.gz" ], - "integrity": "sha256-miM41vja0yRPgj8txghKA+TQ+7J8qJLclw5okNW0gYQ=", + "integrity": "sha256-l27wi0nJKXQfIBeQ5Z44B8cq2B9CjIvJU82+/1/tFes=", "strip_prefix": "", "remote_patches": {}, "remote_patch_strip": 0 } } }, - "bazel_skylib@1.3.0": { - "name": "bazel_skylib", - "version": "1.3.0", - "key": "bazel_skylib@1.3.0", - "repoName": "bazel_skylib", + "rules_license@0.0.7": { + "name": "rules_license", + "version": "0.0.7", + "key": "rules_license@0.0.7", + "repoName": "rules_license", "executionPlatformsToRegister": [], - "toolchainsToRegister": [ - "//toolchains/unittest:cmd_toolchain", - "//toolchains/unittest:bash_toolchain" + "toolchainsToRegister": [], + "extensionUsages": [], + "deps": { + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + }, + "repoSpec": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/bazelbuild/rules_license/releases/download/0.0.7/rules_license-0.0.7.tar.gz" + ], + "integrity": "sha256-RTHezLkTY5ww5cdRKgVNXYdWmNrrddjPkPKEN1/nw2A=", + "strip_prefix": "", + "remote_patches": {}, + "remote_patch_strip": 0 + } + } + }, + "buildozer@6.4.0.2": { + "name": "buildozer", + "version": "6.4.0.2", + "key": "buildozer@6.4.0.2", + "repoName": "buildozer", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [], + "extensionUsages": [ + { + "extensionBzlFile": "@buildozer//:buildozer_binary.bzl", + "extensionName": "buildozer_binary", + "usingModule": "buildozer@6.4.0.2", + "location": { + "file": "https://bcr.bazel.build/modules/buildozer/6.4.0.2/MODULE.bazel", + "line": 7, + "column": 33 + }, + "imports": { + "buildozer_binary": "buildozer_binary" + }, + "devImports": [], + "tags": [ + { + "tagName": "buildozer", + "attributeValues": { + "sha256": { + "darwin-amd64": "d29e347ecd6b5673d72cb1a8de05bf1b06178dd229ff5eb67fad5100c840cc8e", + "darwin-arm64": "9b9e71bdbec5e7223871e913b65d12f6d8fa026684daf991f00e52ed36a6978d", + "linux-amd64": "8dfd6345da4e9042daa738d7fdf34f699c5dfce4632f7207956fceedd8494119", + "linux-arm64": "6559558fded658c8fa7432a9d011f7c4dcbac6b738feae73d2d5c352e5f605fa", + "windows-amd64": "e7f05bf847f7c3689dd28926460ce6e1097ae97380ac8e6ae7147b7b706ba19b" + }, + "version": "6.4.0" + }, + "devDependency": false, + "location": { + "file": "https://bcr.bazel.build/modules/buildozer/6.4.0.2/MODULE.bazel", + "line": 8, + "column": 27 + } + } + ], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + } ], + "deps": { + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + }, + "repoSpec": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/fmeum/buildozer/releases/download/v6.4.0.2/buildozer-v6.4.0.2.tar.gz" + ], + "integrity": "sha256-k7tFKQMR2AygxpmZfH0yEPnQmF3efFgD9rBPkj+Yz/8=", + "strip_prefix": "buildozer-6.4.0.2", + "remote_patches": { + "https://bcr.bazel.build/modules/buildozer/6.4.0.2/patches/module_dot_bazel_version.patch": "sha256-gKANF2HMilj7bWmuXs4lbBIAAansuWC4IhWGB/CerjU=" + }, + "remote_patch_strip": 1 + } + } + }, + "zlib@1.3": { + "name": "zlib", + "version": "1.3", + "key": "zlib@1.3", + "repoName": "zlib", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [], "extensionUsages": [], "deps": { - "platforms": "platforms@0.0.7", + "platforms": "platforms@0.0.9", + "rules_cc": "rules_cc@0.0.9", + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + }, + "repoSpec": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/madler/zlib/releases/download/v1.3/zlib-1.3.tar.gz" + ], + "integrity": "sha256-/wukwpIBPbwnUws6geH5qBPNOd4Byl4Pi/NVcC76WT4=", + "strip_prefix": "zlib-1.3", + "remote_patches": { + "https://bcr.bazel.build/modules/zlib/1.3/patches/add_build_file.patch": "sha256-Ei+FYaaOo7A3jTKunMEodTI0Uw5NXQyZEcboMC8JskY=", + "https://bcr.bazel.build/modules/zlib/1.3/patches/module_dot_bazel.patch": "sha256-fPWLM+2xaF/kuy+kZc1YTfW6hNjrkG400Ho7gckuyJk=" + }, + "remote_patch_strip": 0 + } + } + }, + "stardoc@0.6.2": { + "name": "stardoc", + "version": "0.6.2", + "key": "stardoc@0.6.2", + "repoName": "stardoc", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [], + "extensionUsages": [ + { + "extensionBzlFile": "@rules_jvm_external//:extensions.bzl", + "extensionName": "maven", + "usingModule": "stardoc@0.6.2", + "location": { + "file": "https://bcr.bazel.build/modules/stardoc/0.6.2/MODULE.bazel", + "line": 22, + "column": 22 + }, + "imports": { + "stardoc_maven": "stardoc_maven" + }, + "devImports": [], + "tags": [ + { + "tagName": "install", + "attributeValues": { + "name": "stardoc_maven", + "artifacts": [ + "com.beust:jcommander:1.82", + "com.google.escapevelocity:escapevelocity:1.1", + "com.google.guava:guava:31.1-jre", + "com.google.truth:truth:1.1.3", + "junit:junit:4.13.2" + ], + "fail_if_repin_required": true, + "lock_file": "//:maven_install.json", + "repositories": [ + "https://repo1.maven.org/maven2" + ], + "strict_visibility": true + }, + "devDependency": false, + "location": { + "file": "https://bcr.bazel.build/modules/stardoc/0.6.2/MODULE.bazel", + "line": 23, + "column": 14 + } + } + ], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + } + ], + "deps": { + "bazel_skylib": "bazel_skylib@1.5.0", + "rules_java": "rules_java@7.4.0", + "rules_jvm_external": "rules_jvm_external@5.2", + "rules_license": "rules_license@0.0.7", + "com_google_protobuf": "protobuf@21.7", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" }, "repoSpec": { - "bzlFile": "@bazel_tools//tools/build_defs/repo:http.bzl", + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", "ruleClassName": "http_archive", "attributes": { - "name": "bazel_skylib~1.3.0", "urls": [ - "https://github.com/bazelbuild/bazel-skylib/releases/download/1.3.0/bazel-skylib-1.3.0.tar.gz" + "https://github.com/bazelbuild/stardoc/releases/download/0.6.2/stardoc-0.6.2.tar.gz" ], - "integrity": "sha256-dNVE2W9KW7Yw1GXKi7z+Ix41lOWq5X4e2/F6brPKJQY=", + "integrity": "sha256-Yr0uYCFrem/sOseTQaogHglWR358j2zMKG8nmtHZZDI=", "strip_prefix": "", "remote_patches": {}, "remote_patch_strip": 0 } } - } - }, - "moduleExtensions": {} + }, + "buildifier_prebuilt@6.0.0.1": { + "name": "buildifier_prebuilt", + "version": "6.0.0.1", + "key": "buildifier_prebuilt@6.0.0.1", + "repoName": "buildifier_prebuilt", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [ + "@buildifier_prebuilt_toolchains//:all" + ], + "extensionUsages": [ + { + "extensionBzlFile": "@buildifier_prebuilt//:defs.bzl", + "extensionName": "buildifier_prebuilt_deps_extension", + "usingModule": "buildifier_prebuilt@6.0.0.1", + "location": { + "file": "https://bcr.bazel.build/modules/buildifier_prebuilt/6.0.0.1/MODULE.bazel", + "line": 10, + "column": 32 + }, + "imports": { + "buildifier_prebuilt_toolchains": "buildifier_prebuilt_toolchains" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + } + ], + "deps": { + "bazel_skylib": "bazel_skylib@1.5.0", + "platforms": "platforms@0.0.9", + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + }, + "repoSpec": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/keith/buildifier-prebuilt/archive/refs/tags/6.0.0.1.tar.gz" + ], + "integrity": "sha256-9wk6lgqMNHFVJ2SJLOEsti2bcmAPpMiwiyCQxF2wXOg=", + "strip_prefix": "buildifier-prebuilt-6.0.0.1", + "remote_patches": {}, + "remote_patch_strip": 0 + } + } + }, + "rules_pkg@0.7.0": { + "name": "rules_pkg", + "version": "0.7.0", + "key": "rules_pkg@0.7.0", + "repoName": "rules_pkg", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [], + "extensionUsages": [], + "deps": { + "rules_python": "rules_python@0.27.1", + "bazel_skylib": "bazel_skylib@1.5.0", + "rules_license": "rules_license@0.0.7", + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + }, + "repoSpec": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/bazelbuild/rules_pkg/releases/download/0.7.0/rules_pkg-0.7.0.tar.gz" + ], + "integrity": "sha256-iimOgydi7aGDBZfWT+fbWBeKqEzVkm121bdE1lWJQcI=", + "strip_prefix": "", + "remote_patches": { + "https://bcr.bazel.build/modules/rules_pkg/0.7.0/patches/module_dot_bazel.patch": "sha256-4OaEPZwYF6iC71ZTDg6MJ7LLqX7ZA0/kK4mT+4xKqiE=" + }, + "remote_patch_strip": 0 + } + } + }, + "abseil-cpp@20211102.0": { + "name": "abseil-cpp", + "version": "20211102.0", + "key": "abseil-cpp@20211102.0", + "repoName": "abseil-cpp", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [], + "extensionUsages": [], + "deps": { + "rules_cc": "rules_cc@0.0.9", + "platforms": "platforms@0.0.9", + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + }, + "repoSpec": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/abseil/abseil-cpp/archive/refs/tags/20211102.0.tar.gz" + ], + "integrity": "sha256-3PcbnLqNwMqZQMSzFqDHlr6Pq0KwcLtrfKtitI8OZsQ=", + "strip_prefix": "abseil-cpp-20211102.0", + "remote_patches": { + "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/patches/module_dot_bazel.patch": "sha256-4izqopgGCey4jVZzl/w3M2GVPNohjh2B5TmbThZNvPY=" + }, + "remote_patch_strip": 0 + } + } + }, + "upb@0.0.0-20220923-a547704": { + "name": "upb", + "version": "0.0.0-20220923-a547704", + "key": "upb@0.0.0-20220923-a547704", + "repoName": "upb", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [], + "extensionUsages": [], + "deps": { + "bazel_skylib": "bazel_skylib@1.5.0", + "rules_proto": "rules_proto@5.3.0-21.7", + "com_google_protobuf": "protobuf@21.7", + "com_google_absl": "abseil-cpp@20211102.0", + "platforms": "platforms@0.0.9", + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + }, + "repoSpec": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/protocolbuffers/upb/archive/a5477045acaa34586420942098f5fecd3570f577.tar.gz" + ], + "integrity": "sha256-z39x6v+QskwaKLSWRan/A6mmwecTQpHOcJActj5zZLU=", + "strip_prefix": "upb-a5477045acaa34586420942098f5fecd3570f577", + "remote_patches": { + "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/patches/module_dot_bazel.patch": "sha256-wH4mNS6ZYy+8uC0HoAft/c7SDsq2Kxf+J8dUakXhaB0=" + }, + "remote_patch_strip": 0 + } + } + }, + "rules_jvm_external@5.2": { + "name": "rules_jvm_external", + "version": "5.2", + "key": "rules_jvm_external@5.2", + "repoName": "rules_jvm_external", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [], + "extensionUsages": [ + { + "extensionBzlFile": "@rules_jvm_external//:non-module-deps.bzl", + "extensionName": "non_module_deps", + "usingModule": "rules_jvm_external@5.2", + "location": { + "file": "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel", + "line": 9, + "column": 32 + }, + "imports": { + "io_bazel_rules_kotlin": "io_bazel_rules_kotlin" + }, + "devImports": [], + "tags": [], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + }, + { + "extensionBzlFile": "@rules_jvm_external//:extensions.bzl", + "extensionName": "maven", + "usingModule": "rules_jvm_external@5.2", + "location": { + "file": "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel", + "line": 15, + "column": 22 + }, + "imports": { + "rules_jvm_external_deps": "rules_jvm_external_deps" + }, + "devImports": [], + "tags": [ + { + "tagName": "install", + "attributeValues": { + "name": "rules_jvm_external_deps", + "artifacts": [ + "com.google.auth:google-auth-library-credentials:0.22.0", + "com.google.auth:google-auth-library-oauth2-http:0.22.0", + "com.google.cloud:google-cloud-core:1.93.10", + "com.google.cloud:google-cloud-storage:1.113.4", + "com.google.code.gson:gson:2.9.0", + "com.google.googlejavaformat:google-java-format:1.15.0", + "com.google.guava:guava:31.1-jre", + "org.apache.maven:maven-artifact:3.8.6", + "software.amazon.awssdk:s3:2.17.183" + ], + "lock_file": "@rules_jvm_external//:rules_jvm_external_deps_install.json" + }, + "devDependency": false, + "location": { + "file": "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel", + "line": 16, + "column": 14 + } + } + ], + "hasDevUseExtension": false, + "hasNonDevUseExtension": true + } + ], + "deps": { + "bazel_skylib": "bazel_skylib@1.5.0", + "io_bazel_stardoc": "stardoc@0.6.2", + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + }, + "repoSpec": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/bazelbuild/rules_jvm_external/releases/download/5.2/rules_jvm_external-5.2.tar.gz" + ], + "integrity": "sha256-+G/UKoCeGHHKCqvonbDUQEUSGcPORsWNokDH3NwAEl8=", + "strip_prefix": "rules_jvm_external-5.2", + "remote_patches": {}, + "remote_patch_strip": 0 + } + } + }, + "googletest@1.11.0": { + "name": "googletest", + "version": "1.11.0", + "key": "googletest@1.11.0", + "repoName": "googletest", + "executionPlatformsToRegister": [], + "toolchainsToRegister": [], + "extensionUsages": [], + "deps": { + "com_google_absl": "abseil-cpp@20211102.0", + "platforms": "platforms@0.0.9", + "rules_cc": "rules_cc@0.0.9", + "bazel_tools": "bazel_tools@_", + "local_config_platform": "local_config_platform@_" + }, + "repoSpec": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/google/googletest/archive/refs/tags/release-1.11.0.tar.gz" + ], + "integrity": "sha256-tIcL8SH/d5W6INILzdhie44Ijy0dqymaAxwQNO3ck9U=", + "strip_prefix": "googletest-release-1.11.0", + "remote_patches": { + "https://bcr.bazel.build/modules/googletest/1.11.0/patches/module_dot_bazel.patch": "sha256-HuahEdI/n8KCI071sN3CEziX+7qP/Ec77IWayYunLP0=" + }, + "remote_patch_strip": 0 + } + } + } + }, + "moduleExtensions": { + "@@apple_support~//crosstool:setup.bzl%apple_cc_configure_extension": { + "general": { + "bzlTransitiveDigest": "RyR+EbN4fAzxxZSQKwXXrxEtMVrezn79MOR/2mmcmYk=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "local_config_apple_cc": { + "bzlFile": "@@apple_support~//crosstool:setup.bzl", + "ruleClassName": "_apple_cc_autoconf", + "attributes": {} + }, + "local_config_apple_cc_toolchains": { + "bzlFile": "@@apple_support~//crosstool:setup.bzl", + "ruleClassName": "_apple_cc_autoconf_toolchains", + "attributes": {} + } + }, + "recordedRepoMappingEntries": [ + [ + "apple_support~", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, + "@@bazel_features~//private:extensions.bzl%version_extension": { + "general": { + "bzlTransitiveDigest": "3FcE0iMy2yYKEbEO19f72k9dzcpRUXHH+igow5yVy8g=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "bazel_features_version": { + "bzlFile": "@@bazel_features~//private:version_repo.bzl", + "ruleClassName": "version_repo", + "attributes": {} + }, + "bazel_features_globals": { + "bzlFile": "@@bazel_features~//private:globals_repo.bzl", + "ruleClassName": "globals_repo", + "attributes": { + "globals": { + "RunEnvironmentInfo": "5.3.0", + "DefaultInfo": "0.0.1", + "__TestingOnly_NeverAvailable": "1000000000.0.0" + } + } + } + }, + "recordedRepoMappingEntries": [ + [ + "bazel_features~", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, + "@@bazel_tools//tools/cpp:cc_configure.bzl%cc_configure_extension": { + "general": { + "bzlTransitiveDigest": "PHpT2yqMGms2U4L3E/aZ+WcQalmZWm+ILdP3yiLsDhA=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "local_config_cc": { + "bzlFile": "@@bazel_tools//tools/cpp:cc_configure.bzl", + "ruleClassName": "cc_autoconf", + "attributes": {} + }, + "local_config_cc_toolchains": { + "bzlFile": "@@bazel_tools//tools/cpp:cc_configure.bzl", + "ruleClassName": "cc_autoconf_toolchains", + "attributes": {} + } + }, + "recordedRepoMappingEntries": [ + [ + "bazel_tools", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, + "@@bazel_tools//tools/osx:xcode_configure.bzl%xcode_configure_extension": { + "general": { + "bzlTransitiveDigest": "Qh2bWTU6QW6wkrd87qrU4YeY+SG37Nvw3A0PR4Y0L2Y=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "local_config_xcode": { + "bzlFile": "@@bazel_tools//tools/osx:xcode_configure.bzl", + "ruleClassName": "xcode_autoconf", + "attributes": { + "xcode_locator": "@bazel_tools//tools/osx:xcode_locator.m", + "remote_xcode": "" + } + } + }, + "recordedRepoMappingEntries": [] + } + }, + "@@bazel_tools//tools/sh:sh_configure.bzl%sh_configure_extension": { + "general": { + "bzlTransitiveDigest": "hp4NgmNjEg5+xgvzfh6L83bt9/aiiWETuNpwNuF1MSU=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "local_config_sh": { + "bzlFile": "@@bazel_tools//tools/sh:sh_configure.bzl", + "ruleClassName": "sh_config", + "attributes": {} + } + }, + "recordedRepoMappingEntries": [] + } + }, + "@@buildifier_prebuilt~//:defs.bzl%buildifier_prebuilt_deps_extension": { + "general": { + "bzlTransitiveDigest": "ZLz8nU7kwZ0wfwY66sYRmuSjBB3R2KenLwVQfaHkvRI=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "buildozer_darwin_amd64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_file", + "attributes": { + "urls": [ + "https://github.com/bazelbuild/buildtools/releases/download/6.0.0/buildozer-darwin-amd64" + ], + "downloaded_file_path": "buildozer", + "executable": true, + "sha256": "17c97b23ebf0aa59c3c457800090e5d9b937511bafbe91d22aec972fbdf588d0" + } + }, + "buildifier_linux_amd64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_file", + "attributes": { + "urls": [ + "https://github.com/bazelbuild/buildtools/releases/download/6.0.0/buildifier-linux-amd64" + ], + "downloaded_file_path": "buildifier", + "executable": true, + "sha256": "7ff82176879c0c13bc682b6b0e482d670fbe13bbb20e07915edb0ad11be50502" + } + }, + "buildozer_darwin_arm64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_file", + "attributes": { + "urls": [ + "https://github.com/bazelbuild/buildtools/releases/download/6.0.0/buildozer-darwin-arm64" + ], + "downloaded_file_path": "buildozer", + "executable": true, + "sha256": "8d5e26446cd5a945588b1e0c72854d2cc367fac98d16ddeccbc59b0c87a9a05e" + } + }, + "buildozer_linux_amd64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_file", + "attributes": { + "urls": [ + "https://github.com/bazelbuild/buildtools/releases/download/6.0.0/buildozer-linux-amd64" + ], + "downloaded_file_path": "buildozer", + "executable": true, + "sha256": "b46c12c81ab45306d3bbb4b3a6cd795532d1c3036ed126fbc43fde23d6c35f2d" + } + }, + "buildozer_linux_arm64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_file", + "attributes": { + "urls": [ + "https://github.com/bazelbuild/buildtools/releases/download/6.0.0/buildozer-linux-arm64" + ], + "downloaded_file_path": "buildozer", + "executable": true, + "sha256": "548c3a6c890ef5cc4398d5afeb1399717b43740eb910f7488a36b76440ca0383" + } + }, + "buildifier_prebuilt_toolchains": { + "bzlFile": "@@buildifier_prebuilt~//:defs.bzl", + "ruleClassName": "_buildifier_toolchain_setup", + "attributes": { + "assets_json": "[{\"arch\":\"amd64\",\"name\":\"buildifier\",\"platform\":\"darwin\",\"sha256\":\"3f8ab7dd5d5946ce44695f29c3b895ad11a9a6776c247ad5273e9c8480216ae1\",\"version\":\"6.0.0\"},{\"arch\":\"arm64\",\"name\":\"buildifier\",\"platform\":\"darwin\",\"sha256\":\"21fa0d48ef0b7251eb6e3521cbe25d1e52404763cd2a43aa29f69b5380559dd1\",\"version\":\"6.0.0\"},{\"arch\":\"amd64\",\"name\":\"buildifier\",\"platform\":\"linux\",\"sha256\":\"7ff82176879c0c13bc682b6b0e482d670fbe13bbb20e07915edb0ad11be50502\",\"version\":\"6.0.0\"},{\"arch\":\"arm64\",\"name\":\"buildifier\",\"platform\":\"linux\",\"sha256\":\"9ffa62ea1f55f420c36eeef1427f71a34a5d24332cb861753b2b59c66d6343e2\",\"version\":\"6.0.0\"},{\"arch\":\"amd64\",\"name\":\"buildozer\",\"platform\":\"darwin\",\"sha256\":\"17c97b23ebf0aa59c3c457800090e5d9b937511bafbe91d22aec972fbdf588d0\",\"version\":\"6.0.0\"},{\"arch\":\"arm64\",\"name\":\"buildozer\",\"platform\":\"darwin\",\"sha256\":\"8d5e26446cd5a945588b1e0c72854d2cc367fac98d16ddeccbc59b0c87a9a05e\",\"version\":\"6.0.0\"},{\"arch\":\"amd64\",\"name\":\"buildozer\",\"platform\":\"linux\",\"sha256\":\"b46c12c81ab45306d3bbb4b3a6cd795532d1c3036ed126fbc43fde23d6c35f2d\",\"version\":\"6.0.0\"},{\"arch\":\"arm64\",\"name\":\"buildozer\",\"platform\":\"linux\",\"sha256\":\"548c3a6c890ef5cc4398d5afeb1399717b43740eb910f7488a36b76440ca0383\",\"version\":\"6.0.0\"}]" + } + }, + "buildifier_darwin_amd64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_file", + "attributes": { + "urls": [ + "https://github.com/bazelbuild/buildtools/releases/download/6.0.0/buildifier-darwin-amd64" + ], + "downloaded_file_path": "buildifier", + "executable": true, + "sha256": "3f8ab7dd5d5946ce44695f29c3b895ad11a9a6776c247ad5273e9c8480216ae1" + } + }, + "buildifier_darwin_arm64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_file", + "attributes": { + "urls": [ + "https://github.com/bazelbuild/buildtools/releases/download/6.0.0/buildifier-darwin-arm64" + ], + "downloaded_file_path": "buildifier", + "executable": true, + "sha256": "21fa0d48ef0b7251eb6e3521cbe25d1e52404763cd2a43aa29f69b5380559dd1" + } + }, + "buildifier_linux_arm64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_file", + "attributes": { + "urls": [ + "https://github.com/bazelbuild/buildtools/releases/download/6.0.0/buildifier-linux-arm64" + ], + "downloaded_file_path": "buildifier", + "executable": true, + "sha256": "9ffa62ea1f55f420c36eeef1427f71a34a5d24332cb861753b2b59c66d6343e2" + } + } + }, + "recordedRepoMappingEntries": [ + [ + "buildifier_prebuilt~", + "bazel_skylib", + "bazel_skylib~" + ], + [ + "buildifier_prebuilt~", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, + "@@rules_java~//java:extensions.bzl%toolchains": { + "general": { + "bzlTransitiveDigest": "tJHbmWnq7m+9eUBnUdv7jZziQ26FmcGL9C5/hU3Q9UQ=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "remotejdk21_linux_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_21\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"21\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk21_linux//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk21_linux//:jdk\",\n)\n" + } + }, + "remotejdk17_linux_s390x_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:s390x\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux_s390x//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:s390x\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux_s390x//:jdk\",\n)\n" + } + }, + "remotejdk17_macos_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_macos//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_macos//:jdk\",\n)\n" + } + }, + "remotejdk21_macos_aarch64_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_21\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"21\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk21_macos_aarch64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk21_macos_aarch64//:jdk\",\n)\n" + } + }, + "remotejdk17_linux_aarch64_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux_aarch64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux_aarch64//:jdk\",\n)\n" + } + }, + "remotejdk21_macos_aarch64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 21,\n)\n", + "sha256": "e8260516de8b60661422a725f1df2c36ef888f6fb35393566b00e7325db3d04e", + "strip_prefix": "zulu21.32.17-ca-jdk21.0.2-macosx_aarch64", + "urls": [ + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-macosx_aarch64.tar.gz", + "https://cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-macosx_aarch64.tar.gz" + ] + } + }, + "remotejdk17_linux_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux//:jdk\",\n)\n" + } + }, + "remotejdk17_macos_aarch64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", + "sha256": "314b04568ec0ae9b36ba03c9cbd42adc9e1265f74678923b19297d66eb84dcca", + "strip_prefix": "zulu17.44.53-ca-jdk17.0.8.1-macosx_aarch64", + "urls": [ + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-macosx_aarch64.tar.gz", + "https://cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-macosx_aarch64.tar.gz" + ] + } + }, + "remote_java_tools_windows": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "sha256": "fe2f88169696d6c6fc6e90ba61bb46be7d0ae3693cbafdf336041bf56679e8d1", + "urls": [ + "https://mirror.bazel.build/bazel_java_tools/releases/java/v13.4/java_tools_windows-v13.4.zip", + "https://github.com/bazelbuild/java_tools/releases/download/java_v13.4/java_tools_windows-v13.4.zip" + ] + } + }, + "remotejdk11_win": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", + "sha256": "43408193ce2fa0862819495b5ae8541085b95660153f2adcf91a52d3a1710e83", + "strip_prefix": "zulu11.66.15-ca-jdk11.0.20-win_x64", + "urls": [ + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-win_x64.zip", + "https://cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-win_x64.zip" + ] + } + }, + "remotejdk11_win_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_win//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_win//:jdk\",\n)\n" + } + }, + "remotejdk11_linux_aarch64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", + "sha256": "54174439f2b3fddd11f1048c397fe7bb45d4c9d66d452d6889b013d04d21c4de", + "strip_prefix": "zulu11.66.15-ca-jdk11.0.20-linux_aarch64", + "urls": [ + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-linux_aarch64.tar.gz", + "https://cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-linux_aarch64.tar.gz" + ] + } + }, + "remotejdk17_linux": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", + "sha256": "b9482f2304a1a68a614dfacddcf29569a72f0fac32e6c74f83dc1b9a157b8340", + "strip_prefix": "zulu17.44.53-ca-jdk17.0.8.1-linux_x64", + "urls": [ + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-linux_x64.tar.gz", + "https://cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-linux_x64.tar.gz" + ] + } + }, + "remotejdk11_linux_s390x_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:s390x\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux_s390x//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:s390x\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux_s390x//:jdk\",\n)\n" + } + }, + "remotejdk11_linux_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux//:jdk\",\n)\n" + } + }, + "remotejdk11_macos": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", + "sha256": "bcaab11cfe586fae7583c6d9d311c64384354fb2638eb9a012eca4c3f1a1d9fd", + "strip_prefix": "zulu11.66.15-ca-jdk11.0.20-macosx_x64", + "urls": [ + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-macosx_x64.tar.gz", + "https://cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-macosx_x64.tar.gz" + ] + } + }, + "remotejdk11_win_arm64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", + "sha256": "b8a28e6e767d90acf793ea6f5bed0bb595ba0ba5ebdf8b99f395266161e53ec2", + "strip_prefix": "jdk-11.0.13+8", + "urls": [ + "https://mirror.bazel.build/aka.ms/download-jdk/microsoft-jdk-11.0.13.8.1-windows-aarch64.zip" + ] + } + }, + "remotejdk17_macos": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", + "sha256": "640453e8afe8ffe0fb4dceb4535fb50db9c283c64665eebb0ba68b19e65f4b1f", + "strip_prefix": "zulu17.44.53-ca-jdk17.0.8.1-macosx_x64", + "urls": [ + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-macosx_x64.tar.gz", + "https://cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-macosx_x64.tar.gz" + ] + } + }, + "remotejdk21_macos": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 21,\n)\n", + "sha256": "3ad8fe288eb57d975c2786ae453a036aa46e47ab2ac3d81538ebae2a54d3c025", + "strip_prefix": "zulu21.32.17-ca-jdk21.0.2-macosx_x64", + "urls": [ + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-macosx_x64.tar.gz", + "https://cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-macosx_x64.tar.gz" + ] + } + }, + "remotejdk21_macos_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_21\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"21\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk21_macos//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk21_macos//:jdk\",\n)\n" + } + }, + "remotejdk17_macos_aarch64_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_macos_aarch64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_macos_aarch64//:jdk\",\n)\n" + } + }, + "remotejdk17_win": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", + "sha256": "192f2afca57701de6ec496234f7e45d971bf623ff66b8ee4a5c81582054e5637", + "strip_prefix": "zulu17.44.53-ca-jdk17.0.8.1-win_x64", + "urls": [ + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-win_x64.zip", + "https://cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-win_x64.zip" + ] + } + }, + "remotejdk11_macos_aarch64_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_macos_aarch64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_macos_aarch64//:jdk\",\n)\n" + } + }, + "remotejdk11_linux_ppc64le_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:ppc\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux_ppc64le//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:ppc\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux_ppc64le//:jdk\",\n)\n" + } + }, + "remotejdk21_linux": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 21,\n)\n", + "sha256": "5ad730fbee6bb49bfff10bf39e84392e728d89103d3474a7e5def0fd134b300a", + "strip_prefix": "zulu21.32.17-ca-jdk21.0.2-linux_x64", + "urls": [ + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-linux_x64.tar.gz", + "https://cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-linux_x64.tar.gz" + ] + } + }, + "remote_java_tools_linux": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "sha256": "ba10f09a138cf185d04cbc807d67a3da42ab13d618c5d1ce20d776e199c33a39", + "urls": [ + "https://mirror.bazel.build/bazel_java_tools/releases/java/v13.4/java_tools_linux-v13.4.zip", + "https://github.com/bazelbuild/java_tools/releases/download/java_v13.4/java_tools_linux-v13.4.zip" + ] + } + }, + "remotejdk21_win": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 21,\n)\n", + "sha256": "f7cc15ca17295e69c907402dfe8db240db446e75d3b150da7bf67243cded93de", + "strip_prefix": "zulu21.32.17-ca-jdk21.0.2-win_x64", + "urls": [ + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-win_x64.zip", + "https://cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-win_x64.zip" + ] + } + }, + "remotejdk21_linux_aarch64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 21,\n)\n", + "sha256": "ce7df1af5d44a9f455617c4b8891443fbe3e4b269c777d8b82ed66f77167cfe0", + "strip_prefix": "zulu21.32.17-ca-jdk21.0.2-linux_aarch64", + "urls": [ + "https://cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-linux_aarch64.tar.gz", + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-linux_aarch64.tar.gz" + ] + } + }, + "remotejdk11_linux_aarch64_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux_aarch64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_linux_aarch64//:jdk\",\n)\n" + } + }, + "remotejdk11_linux_s390x": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", + "sha256": "a58fc0361966af0a5d5a31a2d8a208e3c9bb0f54f345596fd80b99ea9a39788b", + "strip_prefix": "jdk-11.0.15+10", + "urls": [ + "https://mirror.bazel.build/github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.15+10/OpenJDK11U-jdk_s390x_linux_hotspot_11.0.15_10.tar.gz", + "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.15+10/OpenJDK11U-jdk_s390x_linux_hotspot_11.0.15_10.tar.gz" + ] + } + }, + "remotejdk17_linux_aarch64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", + "sha256": "6531cef61e416d5a7b691555c8cf2bdff689201b8a001ff45ab6740062b44313", + "strip_prefix": "zulu17.44.53-ca-jdk17.0.8.1-linux_aarch64", + "urls": [ + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-linux_aarch64.tar.gz", + "https://cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-linux_aarch64.tar.gz" + ] + } + }, + "remotejdk17_win_arm64_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:arm64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_win_arm64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:arm64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_win_arm64//:jdk\",\n)\n" + } + }, + "remotejdk11_linux": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", + "sha256": "a34b404f87a08a61148b38e1416d837189e1df7a040d949e743633daf4695a3c", + "strip_prefix": "zulu11.66.15-ca-jdk11.0.20-linux_x64", + "urls": [ + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-linux_x64.tar.gz", + "https://cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-linux_x64.tar.gz" + ] + } + }, + "remotejdk11_macos_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_macos//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:macos\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_macos//:jdk\",\n)\n" + } + }, + "remotejdk17_linux_ppc64le_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:ppc\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux_ppc64le//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:ppc\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_linux_ppc64le//:jdk\",\n)\n" + } + }, + "remotejdk17_win_arm64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", + "sha256": "6802c99eae0d788e21f52d03cab2e2b3bf42bc334ca03cbf19f71eb70ee19f85", + "strip_prefix": "zulu17.44.53-ca-jdk17.0.8.1-win_aarch64", + "urls": [ + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-win_aarch64.zip", + "https://cdn.azul.com/zulu/bin/zulu17.44.53-ca-jdk17.0.8.1-win_aarch64.zip" + ] + } + }, + "remote_java_tools_darwin_arm64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "sha256": "076a7e198ad077f8c7d997986ef5102427fae6bbfce7a7852d2e080ed8767528", + "urls": [ + "https://mirror.bazel.build/bazel_java_tools/releases/java/v13.4/java_tools_darwin_arm64-v13.4.zip", + "https://github.com/bazelbuild/java_tools/releases/download/java_v13.4/java_tools_darwin_arm64-v13.4.zip" + ] + } + }, + "remotejdk17_linux_ppc64le": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", + "sha256": "00a4c07603d0218cd678461b5b3b7e25b3253102da4022d31fc35907f21a2efd", + "strip_prefix": "jdk-17.0.8.1+1", + "urls": [ + "https://mirror.bazel.build/github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.8.1%2B1/OpenJDK17U-jdk_ppc64le_linux_hotspot_17.0.8.1_1.tar.gz", + "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.8.1%2B1/OpenJDK17U-jdk_ppc64le_linux_hotspot_17.0.8.1_1.tar.gz" + ] + } + }, + "remotejdk21_linux_aarch64_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_21\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"21\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk21_linux_aarch64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:linux\", \"@platforms//cpu:aarch64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk21_linux_aarch64//:jdk\",\n)\n" + } + }, + "remotejdk11_win_arm64_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_11\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"11\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:arm64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk11_win_arm64//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:arm64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk11_win_arm64//:jdk\",\n)\n" + } + }, + "local_jdk": { + "bzlFile": "@@rules_java~//toolchains:local_java_repository.bzl", + "ruleClassName": "_local_java_repository_rule", + "attributes": { + "java_home": "", + "version": "", + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = {RUNTIME_VERSION},\n)\n" + } + }, + "remote_java_tools_darwin_x86_64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "sha256": "4523aec4d09c587091a2dae6f5c9bc6922c220f3b6030e5aba9c8f015913cc65", + "urls": [ + "https://mirror.bazel.build/bazel_java_tools/releases/java/v13.4/java_tools_darwin_x86_64-v13.4.zip", + "https://github.com/bazelbuild/java_tools/releases/download/java_v13.4/java_tools_darwin_x86_64-v13.4.zip" + ] + } + }, + "remote_java_tools": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "sha256": "e025fd260ac39b47c111f5212d64ec0d00d85dec16e49368aae82fc626a940cf", + "urls": [ + "https://mirror.bazel.build/bazel_java_tools/releases/java/v13.4/java_tools-v13.4.zip", + "https://github.com/bazelbuild/java_tools/releases/download/java_v13.4/java_tools-v13.4.zip" + ] + } + }, + "remotejdk17_linux_s390x": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 17,\n)\n", + "sha256": "ffacba69c6843d7ca70d572489d6cc7ab7ae52c60f0852cedf4cf0d248b6fc37", + "strip_prefix": "jdk-17.0.8.1+1", + "urls": [ + "https://mirror.bazel.build/github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.8.1%2B1/OpenJDK17U-jdk_s390x_linux_hotspot_17.0.8.1_1.tar.gz", + "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.8.1%2B1/OpenJDK17U-jdk_s390x_linux_hotspot_17.0.8.1_1.tar.gz" + ] + } + }, + "remotejdk17_win_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_17\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"17\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk17_win//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk17_win//:jdk\",\n)\n" + } + }, + "remotejdk11_linux_ppc64le": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", + "sha256": "a8fba686f6eb8ae1d1a9566821dbd5a85a1108b96ad857fdbac5c1e4649fc56f", + "strip_prefix": "jdk-11.0.15+10", + "urls": [ + "https://mirror.bazel.build/github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.15+10/OpenJDK11U-jdk_ppc64le_linux_hotspot_11.0.15_10.tar.gz", + "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.15+10/OpenJDK11U-jdk_ppc64le_linux_hotspot_11.0.15_10.tar.gz" + ] + } + }, + "remotejdk11_macos_aarch64": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@rules_java//java:defs.bzl\", \"java_runtime\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files([\"WORKSPACE\", \"BUILD.bazel\"])\n\nfilegroup(\n name = \"jre\",\n srcs = glob(\n [\n \"jre/bin/**\",\n \"jre/lib/**\",\n ],\n allow_empty = True,\n # In some configurations, Java browser plugin is considered harmful and\n # common antivirus software blocks access to npjp2.dll interfering with Bazel,\n # so do not include it in JRE on Windows.\n exclude = [\"jre/bin/plugin2/**\"],\n ),\n)\n\nfilegroup(\n name = \"jdk-bin\",\n srcs = glob(\n [\"bin/**\"],\n # The JDK on Windows sometimes contains a directory called\n # \"%systemroot%\", which is not a valid label.\n exclude = [\"**/*%*/**\"],\n ),\n)\n\n# This folder holds security policies.\nfilegroup(\n name = \"jdk-conf\",\n srcs = glob(\n [\"conf/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-include\",\n srcs = glob(\n [\"include/**\"],\n allow_empty = True,\n ),\n)\n\nfilegroup(\n name = \"jdk-lib\",\n srcs = glob(\n [\"lib/**\", \"release\"],\n allow_empty = True,\n exclude = [\n \"lib/missioncontrol/**\",\n \"lib/visualvm/**\",\n ],\n ),\n)\n\njava_runtime(\n name = \"jdk\",\n srcs = [\n \":jdk-bin\",\n \":jdk-conf\",\n \":jdk-include\",\n \":jdk-lib\",\n \":jre\",\n ],\n # Provide the 'java` binary explicitly so that the correct path is used by\n # Bazel even when the host platform differs from the execution platform.\n # Exactly one of the two globs will be empty depending on the host platform.\n # When --incompatible_disallow_empty_glob is enabled, each individual empty\n # glob will fail without allow_empty = True, even if the overall result is\n # non-empty.\n java = glob([\"bin/java.exe\", \"bin/java\"], allow_empty = True)[0],\n version = 11,\n)\n", + "sha256": "7632bc29f8a4b7d492b93f3bc75a7b61630894db85d136456035ab2a24d38885", + "strip_prefix": "zulu11.66.15-ca-jdk11.0.20-macosx_aarch64", + "urls": [ + "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-macosx_aarch64.tar.gz", + "https://cdn.azul.com/zulu/bin/zulu11.66.15-ca-jdk11.0.20-macosx_aarch64.tar.gz" + ] + } + }, + "remotejdk21_win_toolchain_config_repo": { + "bzlFile": "@@rules_java~//toolchains:remote_java_repository.bzl", + "ruleClassName": "_toolchain_config", + "attributes": { + "build_file": "\nconfig_setting(\n name = \"prefix_version_setting\",\n values = {\"java_runtime_version\": \"remotejdk_21\"},\n visibility = [\"//visibility:private\"],\n)\nconfig_setting(\n name = \"version_setting\",\n values = {\"java_runtime_version\": \"21\"},\n visibility = [\"//visibility:private\"],\n)\nalias(\n name = \"version_or_prefix_version_setting\",\n actual = select({\n \":version_setting\": \":version_setting\",\n \"//conditions:default\": \":prefix_version_setting\",\n }),\n visibility = [\"//visibility:private\"],\n)\ntoolchain(\n name = \"toolchain\",\n target_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:runtime_toolchain_type\",\n toolchain = \"@remotejdk21_win//:jdk\",\n)\ntoolchain(\n name = \"bootstrap_runtime_toolchain\",\n # These constraints are not required for correctness, but prevent fetches of remote JDK for\n # different architectures. As every Java compilation toolchain depends on a bootstrap runtime in\n # the same configuration, this constraint will not result in toolchain resolution failures.\n exec_compatible_with = [\"@platforms//os:windows\", \"@platforms//cpu:x86_64\"],\n target_settings = [\":version_or_prefix_version_setting\"],\n toolchain_type = \"@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type\",\n toolchain = \"@remotejdk21_win//:jdk\",\n)\n" + } + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_java~", + "bazel_tools", + "bazel_tools" + ], + [ + "rules_java~", + "remote_java_tools", + "rules_java~~toolchains~remote_java_tools" + ] + ] + } + }, + "@@rules_python~//python/extensions:python.bzl%python": { + "general": { + "bzlTransitiveDigest": "7a7jv196EvOxUgMutfPERBnBTrOv3o1Z/xJPUFb/WXc=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "python_3_11_s390x-unknown-linux-gnu": { + "bzlFile": "@@rules_python~//python:repositories.bzl", + "ruleClassName": "python_repository", + "attributes": { + "sha256": "f9f19823dba3209cedc4647b00f46ed0177242917db20fb7fb539970e384531c", + "patches": [], + "platform": "s390x-unknown-linux-gnu", + "python_version": "3.11.6", + "release_filename": "20231002/cpython-3.11.6+20231002-s390x-unknown-linux-gnu-install_only.tar.gz", + "urls": [ + "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6+20231002-s390x-unknown-linux-gnu-install_only.tar.gz" + ], + "distutils_content": "", + "strip_prefix": "python", + "coverage_tool": "", + "ignore_root_user_error": false + } + }, + "python_3_11": { + "bzlFile": "@@rules_python~//python/private:toolchains_repo.bzl", + "ruleClassName": "toolchain_aliases", + "attributes": { + "python_version": "3.11.6", + "user_repository_name": "python_3_11", + "platforms": [ + "aarch64-apple-darwin", + "aarch64-unknown-linux-gnu", + "ppc64le-unknown-linux-gnu", + "s390x-unknown-linux-gnu", + "x86_64-apple-darwin", + "x86_64-pc-windows-msvc", + "x86_64-unknown-linux-gnu" + ] + } + }, + "python_3_11_aarch64-unknown-linux-gnu": { + "bzlFile": "@@rules_python~//python:repositories.bzl", + "ruleClassName": "python_repository", + "attributes": { + "sha256": "3e26a672df17708c4dc928475a5974c3fb3a34a9b45c65fb4bd1e50504cc84ec", + "patches": [], + "platform": "aarch64-unknown-linux-gnu", + "python_version": "3.11.6", + "release_filename": "20231002/cpython-3.11.6+20231002-aarch64-unknown-linux-gnu-install_only.tar.gz", + "urls": [ + "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6+20231002-aarch64-unknown-linux-gnu-install_only.tar.gz" + ], + "distutils_content": "", + "strip_prefix": "python", + "coverage_tool": "", + "ignore_root_user_error": false + } + }, + "python_3_11_aarch64-apple-darwin": { + "bzlFile": "@@rules_python~//python:repositories.bzl", + "ruleClassName": "python_repository", + "attributes": { + "sha256": "916c35125b5d8323a21526d7a9154ca626453f63d0878e95b9f613a95006c990", + "patches": [], + "platform": "aarch64-apple-darwin", + "python_version": "3.11.6", + "release_filename": "20231002/cpython-3.11.6+20231002-aarch64-apple-darwin-install_only.tar.gz", + "urls": [ + "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6+20231002-aarch64-apple-darwin-install_only.tar.gz" + ], + "distutils_content": "", + "strip_prefix": "python", + "coverage_tool": "", + "ignore_root_user_error": false + } + }, + "python_3_11_ppc64le-unknown-linux-gnu": { + "bzlFile": "@@rules_python~//python:repositories.bzl", + "ruleClassName": "python_repository", + "attributes": { + "sha256": "7937035f690a624dba4d014ffd20c342e843dd46f89b0b0a1e5726b85deb8eaf", + "patches": [], + "platform": "ppc64le-unknown-linux-gnu", + "python_version": "3.11.6", + "release_filename": "20231002/cpython-3.11.6+20231002-ppc64le-unknown-linux-gnu-install_only.tar.gz", + "urls": [ + "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6+20231002-ppc64le-unknown-linux-gnu-install_only.tar.gz" + ], + "distutils_content": "", + "strip_prefix": "python", + "coverage_tool": "", + "ignore_root_user_error": false + } + }, + "python_3_11_x86_64-apple-darwin": { + "bzlFile": "@@rules_python~//python:repositories.bzl", + "ruleClassName": "python_repository", + "attributes": { + "sha256": "178cb1716c2abc25cb56ae915096c1a083e60abeba57af001996e8bc6ce1a371", + "patches": [], + "platform": "x86_64-apple-darwin", + "python_version": "3.11.6", + "release_filename": "20231002/cpython-3.11.6+20231002-x86_64-apple-darwin-install_only.tar.gz", + "urls": [ + "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6+20231002-x86_64-apple-darwin-install_only.tar.gz" + ], + "distutils_content": "", + "strip_prefix": "python", + "coverage_tool": "", + "ignore_root_user_error": false + } + }, + "pythons_hub": { + "bzlFile": "@@rules_python~//python/private/bzlmod:pythons_hub.bzl", + "ruleClassName": "hub_repo", + "attributes": { + "default_python_version": "3.11", + "toolchain_prefixes": [ + "_0000_python_3_11_" + ], + "toolchain_python_versions": [ + "3.11" + ], + "toolchain_set_python_version_constraints": [ + "False" + ], + "toolchain_user_repository_names": [ + "python_3_11" + ] + } + }, + "python_versions": { + "bzlFile": "@@rules_python~//python/private:toolchains_repo.bzl", + "ruleClassName": "multi_toolchain_aliases", + "attributes": { + "python_versions": { + "3.11": "python_3_11" + } + } + }, + "python_3_11_x86_64-pc-windows-msvc": { + "bzlFile": "@@rules_python~//python:repositories.bzl", + "ruleClassName": "python_repository", + "attributes": { + "sha256": "3933545e6d41462dd6a47e44133ea40995bc6efeed8c2e4cbdf1a699303e95ea", + "patches": [], + "platform": "x86_64-pc-windows-msvc", + "python_version": "3.11.6", + "release_filename": "20231002/cpython-3.11.6+20231002-x86_64-pc-windows-msvc-shared-install_only.tar.gz", + "urls": [ + "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6+20231002-x86_64-pc-windows-msvc-shared-install_only.tar.gz" + ], + "distutils_content": "", + "strip_prefix": "python", + "coverage_tool": "", + "ignore_root_user_error": false + } + }, + "python_3_11_x86_64-unknown-linux-gnu": { + "bzlFile": "@@rules_python~//python:repositories.bzl", + "ruleClassName": "python_repository", + "attributes": { + "sha256": "ee37a7eae6e80148c7e3abc56e48a397c1664f044920463ad0df0fc706eacea8", + "patches": [], + "platform": "x86_64-unknown-linux-gnu", + "python_version": "3.11.6", + "release_filename": "20231002/cpython-3.11.6+20231002-x86_64-unknown-linux-gnu-install_only.tar.gz", + "urls": [ + "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6+20231002-x86_64-unknown-linux-gnu-install_only.tar.gz" + ], + "distutils_content": "", + "strip_prefix": "python", + "coverage_tool": "", + "ignore_root_user_error": false + } + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_python~", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, + "@@rules_python~//python/private/bzlmod:internal_deps.bzl%internal_deps": { + "general": { + "bzlTransitiveDigest": "eaQ4wDSCMX120Gyjf8GAwHl9I1TWl0raUq3qbaqafjg=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "pypi__wheel": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/b8/8b/31273bf66016be6ad22bb7345c37ff350276cfd46e389a0c2ac5da9d9073/wheel-0.41.2-py3-none-any.whl", + "sha256": "75909db2664838d015e3d9139004ee16711748a52c8f336b52882266540215d8", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__click": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", + "sha256": "ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__importlib_metadata": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/cc/37/db7ba97e676af155f5fcb1a35466f446eadc9104e25b83366e8088c9c926/importlib_metadata-6.8.0-py3-none-any.whl", + "sha256": "3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__pyproject_hooks": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/d5/ea/9ae603de7fbb3df820b23a70f6aff92bf8c7770043254ad8d2dc9d6bcba4/pyproject_hooks-1.0.0-py3-none-any.whl", + "sha256": "283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__pep517": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/ee/2f/ef63e64e9429111e73d3d6cbee80591672d16f2725e648ebc52096f3d323/pep517-0.13.0-py3-none-any.whl", + "sha256": "4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__packaging": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/ab/c3/57f0601a2d4fe15de7a553c00adbc901425661bf048f2a22dfc500caf121/packaging-23.1-py3-none-any.whl", + "sha256": "994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__pip_tools": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/e8/df/47e6267c6b5cdae867adbdd84b437393e6202ce4322de0a5e0b92960e1d6/pip_tools-7.3.0-py3-none-any.whl", + "sha256": "8717693288720a8c6ebd07149c93ab0be1fced0b5191df9e9decd3263e20d85e", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__setuptools": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/4f/ab/0bcfebdfc3bfa8554b2b2c97a555569c4c1ebc74ea288741ea8326c51906/setuptools-68.1.2-py3-none-any.whl", + "sha256": "3d8083eed2d13afc9426f227b24fd1659489ec107c0e86cec2ffdde5c92e790b", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__zipp": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/8c/08/d3006317aefe25ea79d3b76c9650afabaf6d63d1c8443b236e7405447503/zipp-3.16.2-py3-none-any.whl", + "sha256": "679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__colorama": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", + "sha256": "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__build": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/58/91/17b00d5fac63d3dca605f1b8269ba3c65e98059e1fd99d00283e42a454f0/build-0.10.0-py3-none-any.whl", + "sha256": "af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "rules_python_internal": { + "bzlFile": "@@rules_python~//python/private:internal_config_repo.bzl", + "ruleClassName": "internal_config_repo", + "attributes": {} + }, + "pypi__pip": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/50/c2/e06851e8cc28dcad7c155f4753da8833ac06a5c704c109313b8d5a62968a/pip-23.2.1-py3-none-any.whl", + "sha256": "7ccf472345f20d35bdc9d1841ff5f313260c2c33fe417f48c30ac46cccabf5be", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__installer": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl", + "sha256": "05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__more_itertools": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/5a/cb/6dce742ea14e47d6f565589e859ad225f2a5de576d7696e0623b784e226b/more_itertools-10.1.0-py3-none-any.whl", + "sha256": "64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__tomli": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", + "sha256": "939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_python~", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, + "@@rules_swift_package_manager~//:extensions.bzl%swift_deps": { + "general": { + "bzlTransitiveDigest": "YjE3dFjYQ4sj5gn2Iz1cWVK14/ZJ5cmnAUUGjDbACAA=", + "recordedFileInputs": { + "@@//Package.resolved": "6fa0a5ab5dee4c2acbe04fd63786a2cf6202d00f845eaf6d1e08e18de54371e6", + "@@//Package.swift": "dcdee2ad7bd423078b3d64ca8f64819ec28115d20bff39b44c32996b0c9bbd9c" + }, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "swiftpkg_single_factor_auth_swift": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_single_factor_auth_swift", + "commit": "4caaaa858950b25ea420dbba79de6b4c58801db4", + "remote": "https://github.com/Web3Auth/single-factor-auth-swift.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_lnextensionexecutor": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_lnextensionexecutor", + "commit": "c0226dcd7d653d4c22dd16ccd72619c86b610c2d", + "remote": "https://github.com/LeoNatan/LNExtensionExecutor", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_ton_api_swift": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_ton_api_swift", + "commit": "c1d5a7912480d1794097f4fb8241c3176f394384", + "remote": "https://github.com/tonkeeper/ton-api-swift", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_session_manager_swift": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_session_manager_swift", + "commit": "20cc7bff065d7fe53164d17e7714a3f17d4cea2a", + "remote": "https://github.com/Web3Auth/session-manager-swift.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_tweetnacl_swiftwrap": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_tweetnacl_swiftwrap", + "commit": "f8fd111642bf2336b11ef9ea828510693106e954", + "remote": "https://github.com/bitmark-inc/tweetnacl-swiftwrap", + "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", + "attributes": { + "bazel_package_name": "swiftpkg_swiftystorekit", + "commit": "9ce911639680113dac9b554d6243e406a9758ebe", + "remote": "https://github.com/bizz84/SwiftyStoreKit.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_torus_utils_swift": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_torus_utils_swift", + "commit": "608c28404c506983bfec7bbd957632fc0544db8c", + "remote": "https://github.com/torusresearch/torus-utils-swift.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_factory": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_factory", + "commit": "587995f7d5cc667951d635fbf6b4252324ba0439", + "remote": "https://github.com/hmlongco/Factory.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_snapkit": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_snapkit", + "commit": "2842e6e84e82eb9a8dac0100ca90d9444b0307f4", + "remote": "https://github.com/SnapKit/SnapKit.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_ton_swift": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_ton_swift", + "commit": "e4c3def222afc125f7ee83c1569004e31f0cd05c", + "remote": "https://github.com/denis15yo/ton-swift.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_bigint": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_bigint", + "commit": "0ed110f7555c34ff468e72e1686e59721f2b0da6", + "remote": "https://github.com/attaswift/BigInt", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_core_swift": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_core_swift", + "commit": "f03e7c89c56aaafdb3b22a8ab5ebfefb3fb018a6", + "remote": "https://github.com/denis15yo/core-swift.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_walletconnectswiftv2": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_walletconnectswiftv2", + "commit": "6b48ddc4bcde620310c5ee55388c022a19da5a25", + "remote": "https://github.com/WalletConnect/WalletConnectSwiftV2.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_swiftimagereadwrite": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swiftimagereadwrite", + "commit": "5596407d1cf61b953b8e658fa8636a471df3c509", + "remote": "https://github.com/dagronf/SwiftImageReadWrite", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_tkey_ios": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_tkey_ios", + "commit": "c107450f0675351a9a1eaaefe60bcfa285ff1f9e", + "remote": "https://github.com/tkey/tkey-ios.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_navigation_stack_backport": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_navigation_stack_backport", + "commit": "66716ce9c31198931c2275a0b69de2fdaa687e74", + "remote": "https://github.com/denis15yo/navigation-stack-backport.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_starscream": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_starscream", + "commit": "c6bfd1af48efcc9a9ad203665db12375ba6b145a", + "remote": "https://github.com/daltoniam/Starscream.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_sdwebimage": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_sdwebimage", + "commit": "5191b801aca999b704eb93f118f91468b4570571", + "remote": "https://github.com/SDWebImage/SDWebImage.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_subscriptionanalytics_ios": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_subscriptionanalytics_ios", + "commit": "53bfc6c6f26322ec647b87c338a071714ac69420", + "remote": "git@bitbucket.org:mobyrix/subscriptionanalytics-ios.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_swift_argument_parser": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_argument_parser", + "commit": "41982a3656a71c768319979febd796c6fd111d5c", + "remote": "https://github.com/apple/swift-argument-parser", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_swift_collections": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_collections", + "commit": "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", + "remote": "https://github.com/apple/swift-collections", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_swift_qrcode_generator": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_qrcode_generator", + "commit": "5ca09b6a2ad190f94aa3d6ddef45b187f8c0343b", + "remote": "https://github.com/dagronf/swift-qrcode-generator", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_curvelib.swift": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_curvelib.swift", + "commit": "9f88bd5e56d1df443a908f7a7e81ae4f4d9170ea", + "remote": "https://github.com/tkey/curvelib.swift", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_bigdecimal": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_bigdecimal", + "commit": "04d17040e4615fbfda3a882b9881f6841f4bf557", + "remote": "https://github.com/Zollerboy1/BigDecimal.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_swift_openapi_urlsession": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_openapi_urlsession", + "commit": "9229842c63e9fc3bbd32c661d8274b4d9d8715f1", + "remote": "https://github.com/apple/swift-openapi-urlsession.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_nicegram_assistant_ios": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:local_swift_package.bzl", + "ruleClassName": "local_swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_nicegram_assistant_ios", + "path": "/Users/denisshilovich/work/nicegram-assistant-ios" + } + }, + "swiftpkg_fetch_node_details_swift": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_fetch_node_details_swift", + "commit": "4bd96c33ba8d02d9e27190c5c7cedf09cfdfd656", + "remote": "https://github.com/torusresearch/fetch-node-details-swift.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_swift_numerics": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_numerics", + "commit": "0a5bc04095a675662cf24757cc0640aa2204253b", + "remote": "https://github.com/apple/swift-numerics.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_swift_http_types": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_http_types", + "commit": "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd", + "remote": "https://github.com/apple/swift-http-types", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_grdb.swift": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_grdb.swift", + "commit": "afc958017ee4feefd3c61c8e2cddf81d079d2e39", + "remote": "https://github.com/denis15yo/GRDB.swift.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_qrcode": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_qrcode", + "commit": "263f280d2c8144adfb0b6676109846cfc8dd552b", + "remote": "https://github.com/WalletConnect/QRCode.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_floatingpanel": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_floatingpanel", + "commit": "71f419a3cd212afc7615e2179c2fec1df1aa74da", + "remote": "https://github.com/scenee/FloatingPanel", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_r.swift": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_r.swift", + "commit": "4a0f8c97f1baa27d165dc801982c55bbf51126e5", + "remote": "https://github.com/denis15yo/R.swift.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_keychain_swift": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_keychain_swift", + "commit": "d108a1fa6189e661f91560548ef48651ed8d93b9", + "remote": "https://github.com/evgenyneu/keychain-swift.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_cryptoswift": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_cryptoswift", + "commit": "678d442c6f7828def400a70ae15968aef67ef52d", + "remote": "https://github.com/krzyzanowskim/CryptoSwift.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_anycodable": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_anycodable", + "commit": "862808b2070cd908cb04f9aafe7de83d35f81b05", + "remote": "https://github.com/Flight-School/AnyCodable", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_wallet_core": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_wallet_core", + "commit": "a05c01a44251254749801ebeb0fc4f9623c44f10", + "remote": "https://github.com/trustwallet/wallet-core.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_swift_openapi_runtime": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_openapi_runtime", + "commit": "a51b3bd6f2151e9a6f792ca6937a7242c4758768", + "remote": "https://github.com/apple/swift-openapi-runtime", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_xcodeedit": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_xcodeedit", + "commit": "b6b67389a0f1a6fdd9c6457a8ab5b02eaab13c5c", + "remote": "https://github.com/tomlokhorst/XcodeEdit", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, + "swiftpkg_nicegram_wallet_ios": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:local_swift_package.bzl", + "ruleClassName": "local_swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_nicegram_wallet_ios", + "path": "/Users/denisshilovich/work/nicegram-wallet-ios" + } + }, + "swiftpkg_rive_ios": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_rive_ios", + "commit": "52ab8860c3b2264cc44757cb8fc24689f5e1b564", + "remote": "https://github.com/rive-app/rive-ios.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + } + }, + "moduleExtensionMetadata": { + "explicitRootModuleDirectDeps": [ + "swiftpkg_nicegram_assistant_ios", + "swiftpkg_nicegram_wallet_ios" + ], + "explicitRootModuleDirectDevDeps": [], + "useAllRepos": "NO", + "reproducible": false + }, + "recordedRepoMappingEntries": [ + [ + "rules_swift_package_manager~", + "bazel_skylib", + "bazel_skylib~" + ], + [ + "rules_swift_package_manager~", + "bazel_tools", + "bazel_tools" + ], + [ + "rules_swift_package_manager~", + "cgrindel_bazel_starlib", + "cgrindel_bazel_starlib~" + ] + ] + } + }, + "@@rules_swift~//swift:extensions.bzl%non_module_deps": { + "general": { + "bzlTransitiveDigest": "QgKzLzLwbmI9NGTblAH/DxdVM/oxwn6wSNO3wtXZiWM=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "com_github_grpc_grpc_swift": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/grpc/grpc-swift/archive/1.16.0.tar.gz" + ], + "sha256": "58b60431d0064969f9679411264b82e40a217ae6bd34e17096d92cc4e47556a5", + "strip_prefix": "grpc-swift-1.16.0/", + "build_file": "@@rules_swift~//third_party:com_github_grpc_grpc_swift/BUILD.overlay" + } + }, + "com_github_apple_swift_nio_extras": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/apple/swift-nio-extras/archive/1.4.0.tar.gz" + ], + "sha256": "4684b52951d9d9937bb3e8ccd6b5daedd777021ef2519ea2f18c4c922843b52b", + "strip_prefix": "swift-nio-extras-1.4.0/", + "build_file": "@@rules_swift~//third_party:com_github_apple_swift_nio_extras/BUILD.overlay" + } + }, + "com_github_apple_swift_protobuf": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/apple/swift-protobuf/archive/1.20.2.tar.gz" + ], + "sha256": "3fb50bd4d293337f202d917b6ada22f9548a0a0aed9d9a4d791e6fbd8a246ebb", + "strip_prefix": "swift-protobuf-1.20.2/", + "build_file": "@@rules_swift~//third_party:com_github_apple_swift_protobuf/BUILD.overlay" + } + }, + "com_github_apple_swift_nio_ssl": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/apple/swift-nio-ssl/archive/2.23.0.tar.gz" + ], + "sha256": "4787c63f61dd04d99e498adc3d1a628193387e41efddf8de19b8db04544d016d", + "strip_prefix": "swift-nio-ssl-2.23.0/", + "build_file": "@@rules_swift~//third_party:com_github_apple_swift_nio_ssl/BUILD.overlay" + } + }, + "com_github_apple_swift_atomics": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/apple/swift-atomics/archive/1.1.0.tar.gz" + ], + "sha256": "1bee7f469f7e8dc49f11cfa4da07182fbc79eab000ec2c17bfdce468c5d276fb", + "strip_prefix": "swift-atomics-1.1.0/", + "build_file": "@@rules_swift~//third_party:com_github_apple_swift_atomics/BUILD.overlay" + } + }, + "com_github_apple_swift_nio_http2": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/apple/swift-nio-http2/archive/1.26.0.tar.gz" + ], + "sha256": "f0edfc9d6a7be1d587e5b403f2d04264bdfae59aac1d74f7d974a9022c6d2b25", + "strip_prefix": "swift-nio-http2-1.26.0/", + "build_file": "@@rules_swift~//third_party:com_github_apple_swift_nio_http2/BUILD.overlay" + } + }, + "com_github_apple_swift_nio_transport_services": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/apple/swift-nio-transport-services/archive/1.15.0.tar.gz" + ], + "sha256": "f3498dafa633751a52b9b7f741f7ac30c42bcbeb3b9edca6d447e0da8e693262", + "strip_prefix": "swift-nio-transport-services-1.15.0/", + "build_file": "@@rules_swift~//third_party:com_github_apple_swift_nio_transport_services/BUILD.overlay" + } + }, + "build_bazel_rules_swift_index_import": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file": "@@rules_swift~//third_party:build_bazel_rules_swift_index_import/BUILD.overlay", + "canonical_id": "index-import-5.8", + "urls": [ + "https://github.com/MobileNativeFoundation/index-import/releases/download/5.8.0.1/index-import.tar.gz" + ], + "sha256": "28c1ffa39d99e74ed70623899b207b41f79214c498c603915aef55972a851a15" + } + }, + "com_github_apple_swift_nio": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/apple/swift-nio/archive/2.42.0.tar.gz" + ], + "sha256": "e3304bc3fb53aea74a3e54bd005ede11f6dc357117d9b1db642d03aea87194a0", + "strip_prefix": "swift-nio-2.42.0/", + "build_file": "@@rules_swift~//third_party:com_github_apple_swift_nio/BUILD.overlay" + } + }, + "com_github_apple_swift_log": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/apple/swift-log/archive/1.4.4.tar.gz" + ], + "sha256": "48fe66426c784c0c20031f15dc17faf9f4c9037c192bfac2f643f65cb2321ba0", + "strip_prefix": "swift-log-1.4.4/", + "build_file": "@@rules_swift~//third_party:com_github_apple_swift_log/BUILD.overlay" + } + }, + "com_github_apple_swift_collections": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/apple/swift-collections/archive/1.0.4.tar.gz" + ], + "sha256": "d9e4c8a91c60fb9c92a04caccbb10ded42f4cb47b26a212bc6b39cc390a4b096", + "strip_prefix": "swift-collections-1.0.4/", + "build_file": "@@rules_swift~//third_party:com_github_apple_swift_collections/BUILD.overlay" + } + }, + "build_bazel_rules_swift_local_config": { + "bzlFile": "@@rules_swift~//swift/internal:swift_autoconfiguration.bzl", + "ruleClassName": "swift_autoconfiguration", + "attributes": {} + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_swift~", + "bazel_tools", + "bazel_tools" + ], + [ + "rules_swift~", + "build_bazel_rules_swift", + "rules_swift~" + ] + ] + } + }, + "@@rules_xcodeproj~//xcodeproj:extensions.bzl%internal": { + "general": { + "bzlTransitiveDigest": "tSHwCORMSaTwtem/RoFY8getSILf68ZYVjUaC2RnlrQ=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "rules_xcodeproj_generated": { + "bzlFile": "@@rules_xcodeproj~//xcodeproj:repositories.bzl", + "ruleClassName": "generated_files_repo", + "attributes": {} + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_xcodeproj~", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, + "@@rules_xcodeproj~//xcodeproj:extensions.bzl%non_module_deps": { + "general": { + "bzlTransitiveDigest": "tSHwCORMSaTwtem/RoFY8getSILf68ZYVjUaC2RnlrQ=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "rules_xcodeproj_index_import": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@bazel_skylib//rules:native_binary.bzl\", \"native_binary\")\n\nnative_binary(\n name = \"index_import\",\n src = \"index-import\",\n out = \"index-import\",\n visibility = [\"//visibility:public\"],\n)\n", + "sha256": "28c1ffa39d99e74ed70623899b207b41f79214c498c603915aef55972a851a15", + "url": "https://github.com/MobileNativeFoundation/index-import/releases/download/5.8.0.1/index-import.tar.gz" + } + }, + "com_github_tadija_aexml": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@build_bazel_rules_swift//swift:swift.bzl\", \"swift_library\")\n\nswift_library(\n name = \"AEXML\",\n srcs = glob([\"Sources/AEXML/**/*.swift\"]),\n visibility = [\"//visibility:public\"],\n)\n", + "sha256": "5a76c28e4fa9dcc1cbfb87a8518652628e990e522ecfbc98bdad17eabf4631d5", + "strip_prefix": "AEXML-4.6.1", + "url": "https://github.com/tadija/AEXML/archive/refs/tags/4.6.1.tar.gz" + } + }, + "com_github_michaeleisel_jjliso8601dateformatter": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "objc_library(\n name = \"JJLISO8601DateFormatter\",\n srcs = glob([\"Sources/JJLISO8601DateFormatter/**/*\"]),\n copts = [\n \"-Wno-incompatible-pointer-types\",\n \"-Wno-incompatible-pointer-types-discards-qualifiers\",\n \"-Wno-shorten-64-to-32\",\n \"-Wno-unreachable-code\",\n \"-Wno-unused-function\",\n \"-Wno-unused-variable\",\n ],\n includes = [\"Sources/JJLISO8601DateFormatter/include\"],\n hdrs = glob([\"Sources/JJLISO8601DateFormatter/include/*\"]),\n visibility = [\"//visibility:public\"],\n)\n", + "patches": [ + "@@rules_xcodeproj~//third_party/com_github_michaeleisel_jjliso8601dateformatter:include_fix.patch" + ], + "sha256": "6fe15f251f100f3df057c2802a50765387674fde9c922375683682b5ba37eef0", + "strip_prefix": "JJLISO8601DateFormatter-0.1.6", + "url": "https://github.com/michaeleisel/JJLISO8601DateFormatter/archive/refs/tags/0.1.6.tar.gz" + } + }, + "com_github_michaeleisel_zippyjson": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@build_bazel_rules_swift//swift:swift.bzl\", \"swift_library\")\n\nswift_library(\n name = \"ZippyJSON\",\n srcs = glob([\"Sources/ZippyJSON/**/*.swift\"]),\n deps = [\n \"@com_github_michaeleisel_jjliso8601dateformatter//:JJLISO8601DateFormatter\",\n \"@com_github_michaeleisel_zippyjsoncfamily//:ZippyJSONCFamily\",\n ],\n visibility = [\"//visibility:public\"],\n)\n", + "sha256": "4b256843c9c3686c527e76dde54f8d76b6201c1fd903c07dc2211ab1b250bd04", + "strip_prefix": "ZippyJSON-1.2.10", + "url": "https://github.com/michaeleisel/ZippyJSON/archive/refs/tags/1.2.10.tar.gz" + } + }, + "com_github_michaeleisel_zippyjsoncfamily": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "objc_library(\n name = \"ZippyJSONCFamily\",\n copts = [\n \"-std=c++17\",\n \"-Wno-unused-function\",\n \"-Wno-reorder-ctor\",\n \"-Wno-return-type-c-linkage\",\n \"-Wno-shorten-64-to-32\",\n \"-Wno-unused-variable\",\n ],\n srcs = glob([\"Sources/ZippyJSONCFamily/**/*\"]),\n includes = [\"Sources/ZippyJSONCFamily/include\"],\n hdrs = glob([\"Sources/ZippyJSONCFamily/include/*\"]),\n visibility = [\"//visibility:public\"],\n)\n", + "patches": [ + "@@rules_xcodeproj~//third_party/com_github_michaeleisel_zippyjsoncfamily:include_fix.patch" + ], + "sha256": "b215927ada8403e1b056d39450c6a7b59122eca4b0c7fc5beb5f0b5fea2acd72", + "strip_prefix": "ZippyJSONCFamily-1.2.9", + "url": "https://github.com/michaeleisel/ZippyJSONCFamily/archive/refs/tags/1.2.9.tar.gz" + } + }, + "com_github_kylef_pathkit": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@build_bazel_rules_swift//swift:swift.bzl\", \"swift_library\")\n\nswift_library(\n name = \"PathKit\",\n srcs = glob([\"Sources/**/*.swift\"]),\n visibility = [\"//visibility:public\"],\n)\n", + "sha256": "fcda78cdf12c1c6430c67273333e060a9195951254230e524df77841a0235dae", + "strip_prefix": "PathKit-1.0.1", + "url": "https://github.com/kylef/PathKit/archive/refs/tags/1.0.1.tar.gz" + } + }, + "com_github_tuist_xcodeproj": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@build_bazel_rules_swift//swift:swift.bzl\", \"swift_library\")\n\nswift_library(\n name = \"XcodeProj\",\n srcs = glob([\"Sources/XcodeProj/**/*.swift\"]),\n visibility = [\"//visibility:public\"],\n deps = [\n \"@com_github_tadija_aexml//:AEXML\",\n \"@com_github_kylef_pathkit//:PathKit\",\n ],\n)\n", + "sha256": "70a4504d5cfd30e1c1968df3929bf0c40cba91bdb2ef0e3143c0e72bbe1d8092", + "strip_prefix": "XcodeProj-8.9.0", + "url": "https://github.com/tuist/XcodeProj/archive/refs/tags/8.9.0.tar.gz" + } + }, + "com_github_apple_swift_argument_parser": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@build_bazel_rules_swift//swift:swift.bzl\", \"swift_library\")\n\nswift_library(\n name = \"ArgumentParserToolInfo\",\n srcs = glob([\"Sources/ArgumentParserToolInfo/**/*.swift\"]),\n visibility = [\"//visibility:public\"],\n)\n\nswift_library(\n name = \"ArgumentParser\",\n srcs = glob([\"Sources/ArgumentParser/**/*.swift\"]),\n visibility = [\"//visibility:public\"],\n deps = [\":ArgumentParserToolInfo\"],\n)\n", + "sha256": "4a10bbef290a2167c5cc340b39f1f7ff6a8cf4e1b5433b68548bf5f1e542e908", + "strip_prefix": "swift-argument-parser-1.2.3", + "url": "https://github.com/apple/swift-argument-parser/archive/refs/tags/1.2.3.tar.gz" + } + }, + "com_github_apple_swift_collections": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file_content": "load(\"@build_bazel_rules_swift//swift:swift.bzl\", \"swift_library\")\n\nswift_library(\n name = \"Collections\",\n srcs = glob([\"Sources/Collections/**/*.swift\"]),\n deps = [\n \"@com_github_apple_swift_collections//:DequeModule\",\n ],\n visibility = [\"//visibility:public\"],\n)\n\nswift_library(\n name = \"DequeModule\",\n srcs = glob([\"Sources/DequeModule/**/*.swift\"]),\n visibility = [\"//visibility:public\"],\n)\n\nswift_library(\n name = \"OrderedCollections\",\n srcs = glob([\"Sources/OrderedCollections/**/*.swift\"]),\n visibility = [\"//visibility:public\"],\n)\n", + "sha256": "1a2ec8cc6c63c383a9dd4eb975bf83ce3bc7a2ac21a0289a50dae98a576327d6", + "strip_prefix": "swift-collections-4cab1c1c417855b90e9cfde40349a43aff99c536", + "url": "https://github.com/apple/swift-collections/archive/4cab1c1c417855b90e9cfde40349a43aff99c536.tar.gz" + } + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_xcodeproj~", + "bazel_tools", + "bazel_tools" + ] + ] + } + } + } } diff --git a/Package.resolved b/Package.resolved index 77954116c62..db30e8cb962 100644 --- a/Package.resolved +++ b/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", "state" : { - "revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", - "version" : "1.8.2" + "revision" : "678d442c6f7828def400a70ae15968aef67ef52d", + "version" : "1.8.3" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/scenee/FloatingPanel", "state" : { - "revision" : "29185a47bd9f062c060e097641b863ef07f60ba7", - "version" : "2.8.4" + "revision" : "71f419a3cd212afc7615e2179c2fec1df1aa74da", + "version" : "2.8.5" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "state" : { - "branch" : "next-wallet-release", - "revision" : "033d4882165ee343a5db09c93cbca462f27f5732" + "branch" : "develop", + "revision" : "80900ff4ea20225cea77bd53d6990f9cb4ae8591" } }, { @@ -131,14 +131,14 @@ "kind" : "remoteSourceControl", "location" : "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", "state" : { - "branch" : "next-wallet-release", - "revision" : "2d647862a3111bfc0f6c0f9e966fdc8ff1a21d34" + "branch" : "develop", + "revision" : "98f6e67254017add10b2f460aea75d42dbe8eb34" } }, { "identity" : "qrcode", "kind" : "remoteSourceControl", - "location" : "https://github.com/WalletConnect/QRCode", + "location" : "https://github.com/WalletConnect/QRCode.git", "state" : { "revision" : "263f280d2c8144adfb0b6676109846cfc8dd552b", "version" : "14.3.1" @@ -153,13 +153,22 @@ "revision" : "4a0f8c97f1baa27d165dc801982c55bbf51126e5" } }, + { + "identity" : "rive-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/rive-app/rive-ios.git", + "state" : { + "revision" : "52ab8860c3b2264cc44757cb8fc24689f5e1b564", + "version" : "5.11.3" + } + }, { "identity" : "sdwebimage", "kind" : "remoteSourceControl", "location" : "https://github.com/SDWebImage/SDWebImage.git", "state" : { - "revision" : "86e9185ef41c4238a93ad8efe61ddeb701e80bbf", - "version" : "5.19.5" + "revision" : "5191b801aca999b704eb93f118f91468b4570571", + "version" : "5.19.6" } }, { @@ -338,17 +347,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/trustwallet/wallet-core.git", "state" : { - "revision" : "f14ae4c31e652b293bd5d892b128afc0fa6102c5", - "version" : "4.1.1" + "revision" : "a05c01a44251254749801ebeb0fc4f9623c44f10", + "version" : "4.1.4" } }, { "identity" : "walletconnectswiftv2", "kind" : "remoteSourceControl", - "location" : "https://github.com/denis15yo/WalletConnectSwiftV2.git", + "location" : "https://github.com/WalletConnect/WalletConnectSwiftV2.git", "state" : { - "branch" : "develop", - "revision" : "1eacd732e321c9511859d7e73303d61d82af4d46" + "revision" : "6b48ddc4bcde620310c5ee55388c022a19da5a25", + "version" : "1.19.5" } }, { diff --git a/Package.swift b/Package.swift index dedb727ca76..158b303d6ee 100644 --- a/Package.swift +++ b/Package.swift @@ -5,6 +5,7 @@ import PackageDescription let package = Package( name: "nicegram-package", dependencies: [ - .package(url: "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", branch: "next-wallet-release"), + .package(url: "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", branch: "develop"), + .package(url: "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", branch: "develop"), ] ) diff --git a/Telegram/BUILD b/Telegram/BUILD index 123bde7ec14..01ec4e08bd5 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -456,6 +456,7 @@ plist_fragment( CFBundleURLSchemes tg + tonsite ng ngcn ncg diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 1673172196c..bb7f96225de 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -1710,7 +1710,7 @@ private final class NotificationServiceHandler { } else if let file = media as? TelegramMediaFile { resource = file.resource for attribute in file.attributes { - if case let .Video(_, _, _, preloadSize) = attribute { + if case let .Video(_, _, _, preloadSize, _) = attribute { fetchSize = preloadSize.flatMap(Int64.init) } } diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 9a6e7054cd1..3291c1b68d5 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9587,6 +9587,7 @@ Sorry for the inconvenience."; "Story.ContextPrivacy.LabelEveryone" = "Everyone"; "Story.Context.Privacy" = "Who Can See"; "Story.Context.Edit" = "Edit Story"; +"Story.Context.EditCover" = "Edit Cover"; "Story.Context.SaveToProfile" = "Save to Profile"; "Story.Context.RemoveFromProfile" = "Remove from Profile"; "Story.ToastRemovedFromProfileText" = "Story removed from your profile"; @@ -12456,6 +12457,7 @@ Sorry for the inconvenience."; "Monetization.StarsProceeds.Available" = "Available Balance"; "Monetization.StarsProceeds.Current" = "Total Balance"; "Monetization.StarsProceeds.Total" = "Lifetime Proceeds"; +"Monetization.StarsProceeds.Info" = "Funds from your total balance can be used for ads or withdrawn as rewards 21 days after they are earned."; "Premium.MessageEffects" = "Message Effects"; "Premium.MessageEffectsInfo" = "Add over 500 animated effects to private messages."; @@ -12473,8 +12475,200 @@ Sorry for the inconvenience."; "WebApp.MinimizedTitle.Others_1" = "%@ Other"; "WebApp.MinimizedTitle.Others_any" = "%@ Others"; +"WebApp.Minimized.CloseAllTitle" = "Are you sure you want to close all open tabs?"; +"WebApp.Minimized.CloseAll_1" = "Close All %@ Tab"; +"WebApp.Minimized.CloseAll_any" = "Close All %@ Tabs"; + "Stars.SendStars.Title" = "Send Stars"; "Stars.SendStars.AmountTitle" = "ENTER AMOUNT"; "Stars.SendStars.AmountPlaceholder" = "Stars Amount"; "Stars.SendStars.AmountInfo" = "Send %@ or more to highlight your profile in the TOP 3 supporters of this message."; "Stars.SendStars.SendStars" = "Confirm and Send"; + +"ChatList.DeleteForMe" = "Delete for me"; +"ChatList.DeleteForAllWhenPossible" = "Delete from both sides where possible"; +"ChatList.DeleteForAll" = "Delete from both sides"; + +"WebApp.Miniapp" = "miniapp"; +"WebApp.Share" = "Share"; + +"Stars.Purchase.GiftStars" = "Gift Stars"; +"Stars.Purchase.GiftInfo" = "With Stars, **%1$@** will be able to unlock content and services on Telegram. [See Examples >]()"; + +"Stars.Purchase.SubscriptionInfo" = "Buy Stars to subscribe for **%@**."; +"Stars.Purchase.SubscriptionRenewInfo" = "Buy Stars to keep your subscription for **%@**."; + +"InviteLink.Create.Subscription" = "Require Monthly Fee"; +"InviteLink.Create.Subscription.Placeholder" = "Stars amount per month"; +"InviteLink.Create.Subscription.PerMonth" = "%@ / month"; +"InviteLink.Create.Subscription.Info" = "Charge a subscription fee from people joining your channel via this link. [Learn More >]()"; +"InviteLink.Create.Subscription.EditInfo" = "If you need to change the subscription fee, create a new invite link with a different price."; + +"InviteLink.Create.RequestApprovalUnavailableInfo" = "You can't enable admin approval for links that require a monthly fee."; + +"InviteLink.Subscription.Fee" = "Subscription Fee"; +"InviteLink.Subscription.Info" = "You get approximately %@ monthly"; + +"Notification.StarsGift.Sent" = "%1$@ sent you a gift for %2$@"; +"Notification.StarsGift.SentYou" = "You sent a gift for %@"; + +"Notification.StarsGift.Title_1" = "%@ Star"; +"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."; + +"Bot.Settings" = "Bot Settings"; + +"Browser.ErrorTitle" = "An Error Occurred"; + +"Browser.ContextMenu.Open" = "Open"; +"Browser.ContextMenu.OpenInNewTab" = "Open in New Tab"; +"Browser.ContextMenu.AddToReadingList" = "Add to Reading List"; +"Browser.ContextMenu.CopyLink" = "Copy Link"; +"Browser.ContextMenu.Share" = "Share"; + +"WebBrowser.Telegram" = "Telegram"; + +"Monetization.Proceeds.Ton.Info" = "TON from your total balance can be used for ads or withdrawn as rewards 3 days after they are earned."; +"Monetization.Proceeds.Stars.Info" = "Stars from your total balance can be used for ads or withdrawn as rewards 21 days after they are earned."; +"Monetization.Proceeds.TonAndStars.Info" = "Stars and TON from your total balance can be used for ads or withdrawn as rewards 21 and 3 days respectively after they are earned."; + +"Stars.Transaction.FragmentUnknown_URL" = "https://fragment.com/stars"; +"Conversation.StatusBotSubscribers_1" = "1 user"; +"Conversation.StatusBotSubscribers_any" = "%d users"; + +"Story.Editor.Add" = "Add"; + +"WebBrowser.LinkForwardTooltip.Chat.One" = "Link forwarded to **%@**"; +"WebBrowser.LinkForwardTooltip.TwoChats.One" = "Link forwarded to **%@** and **%@**"; +"WebBrowser.LinkForwardTooltip.ManyChats.One" = "Link forwarded to **%@** and %@ others"; +"WebBrowser.LinkForwardTooltip.SavedMessages.One" = "Link forwarded to **Saved Messages**"; + +"Stars.Intro.StarsSent_1" = "%@ Star sent."; +"Stars.Intro.StarsSent_any" = "%@ Stars sent."; +"Stars.Intro.StarsSent.ViewChat" = "View Chat"; + +"Stars.Gift.Received.Title" = "Received Gift"; +"Stars.Gift.Received.Text" = "Use Stars to unlock content and services on Telegram. [See Examples >]()"; + +"Stars.Gift.Sent.Title" = "Sent Gift"; +"Stars.Gift.Sent.Text" = "With Stars, %@ will be able to unlock content and services on Telegram. [See Examples >]()"; + +"WebBrowser.Reload" = "Reload"; +"WebBrowser.Share" = "Share"; +"WebBrowser.AddBookmark" = "Add Bookmark"; + +"WebBrowser.LinkAddedToBookmarks" = "Link added to [Bookmarks]() and **Saved Messages**."; + +"WebBrowser.AddressBar.RecentlyVisited" = "RECENTLY VISITED"; +"WebBrowser.AddressBar.RecentlyVisited.Clear" = "Clear"; + +"WebBrowser.AddressBar.Bookmarks" = "BOOKMARKS"; +"WebBrowser.AddressBar.ShowMore" = "Show More"; + +"WebBrowser.OpenLinksIn.Title" = "OPEN LINKS IN"; +"WebBrowser.AutoLogin" = "Auto-Login via Telegram"; +"WebBrowser.AutoLogin.Info" = "Use your Telegram account to automatically log in to websites opened in the in-app browser."; + +"WebBrowser.ClearCookies" = "Clear Cookies"; +"WebBrowser.ClearCookies.Info" = "Delete all cookies in the Telegram in-app browser. This action will sign you out of most websites."; +"WebBrowser.ClearCookies.Succeed" = "Cookies cleared."; + +"WebBrowser.Exceptions.Title" = "NEVER OPEN IN THE IN-APP BROWSER"; +"WebBrowser.Exceptions.AddException" = "Add Website"; +"WebBrowser.Exceptions.Clear" = "Clear List"; +"WebBrowser.Exceptions.Info" = "These websites will be always opened in your default browser."; + +"WebBrowser.Exceptions.Create.Title" = "Add Website"; +"WebBrowser.Exceptions.Create.Text" = "Enter a domain that you don't want to be opened in the in-app browser."; +"WebBrowser.Exceptions.Create.Placeholder" = "Enter URL"; + +"WebBrowser.Exceptions.ClearConfirmation.Text" = "Are you sure you want to clear this list?"; +"WebBrowser.Exceptions.ClearConfirmation.Clear" = "Clear"; + +"WebBrowser.ClearCookies.ClearConfirmation.Text" = "Are you sure you want to clear cookies?"; +"WebBrowser.ClearCookies.ClearConfirmation.Clear" = "Clear"; + +"WebBrowser.ClearCache" = "Clear Cache"; +"WebBrowser.ClearCache.ClearConfirmation.Text" = "Are you sure you want to clear cookies?"; +"WebBrowser.ClearCache.ClearConfirmation.Clear" = "Clear"; +"WebBrowser.ClearCache.Succeed" = "Cookies cleared."; + + +"WebBrowser.Done" = "Done"; + +"AccessDenied.LocationWeather" = "Telegram needs access to your location so that you can add the weather widget to your stories.\n\nPlease go to Settings > Privacy > Location Services and set Telegram to ON."; + +"Story.Editor.TooltipWeatherLimitText" = "You can't add more than one weather sticker to a story."; + +"WebBrowser.AddressPlaceholder" = "Enter URL"; + +"WebBrowser.Bookmarks.Title" = "Bookmarks"; +"WebBrowser.Bookmarks.BookmarkCurrent" = "Bookmark Current Page"; + +"Story.Privacy.ChooseCover" = "Choose Story Cover"; +"Story.Privacy.ChooseCoverInfo" = "Choose a frame from the story to show in your Profile."; +"Story.Privacy.ChooseCoverChannelInfo" = "Choose a frame from the story to show in channel profile."; +"Story.Privacy.ChooseCoverGroupInfo" = "Choose a frame from the story to show in group profile."; + +"ChatList.Search.FilterApps" = "Apps"; +"ChatList.Search.SectionPopularApps" = "POPULAR APPS"; +"ChatList.Search.SectionRecentApps" = "APPS YOU USE"; +"ChatList.Search.Apps.Empty.Text" = "No Apps Found"; + +"VoiceChat.MicrophoneModes" = "Microphone Modes"; + +"MiniAppList.Title" = "Examples"; +"MiniAppList.ListSectionHeader" = "APPS THAT ACCEPT STARS"; + +"PeerInfo.PaneBotPreviews" = "Preview"; +"PeerInfo.OpenAppButton" = "Open App"; +"PeerInfo.AppFooterAdmin" = "By publishing this mini app, you agree to the [Telegram Terms of Service for Developers](https://telegram.org/privacy)."; +"PeerInfo.AppFooter" = "By launching this mini app, you agree to the [Terms of Service for Mini Apps](https://telegram.org/privacy)."; +"BotPreviews.MenuAddPreview" = "Add Preview"; +"BotPreviews.MenuReorder" = "Reorder"; +"BotPreviews.MenuDeleteLanguage" = "Delete %@"; +"BotPreviews.SubtitleLoading" = "loading"; +"BotPreviews.SubtitleEmpty" = "no preview added"; +"BotPreviews.SubtitleCount_1" = "1 preview"; +"BotPreviews.SubtitleCount_any" = "%d previews"; +"BotPreviews.SheetDeleteTitle_1" = "Delete 1 Preview?"; +"BotPreviews.SheetDeleteTitle_any" = "Delete %d Previews?"; +"BotPreviews.LanguageTab.Main" = "Main"; +"BotPreviews.LanguageTab.Add" = "+ Add Language"; +"BotPreviews.Empty.Title" = "No Preview"; +"BotPreviews.Empty.Text_1" = "Upload up to 1 screenshot and video demos for your mini app."; +"BotPreviews.Empty.Text_any" = "Upload up to %d screenshots and video demos for your mini app."; +"BotPreviews.Empty.Add" = "Add Preview"; +"BotPreviews.Empty.AddTranslation" = "Create a Translation"; +"BotPreviews.Empty.DeleteTranslation" = "Delete this Translation"; +"BotPreviews.Empty.Separator" = "or"; +"BotPreviews.AlertTooManyPreviews_1" = "You can add at most 1 preview."; +"BotPreviews.AlertTooManyPreviews_any" = "You can add at most 1 previews."; +"BotPreviews.DeleteTranslationAlert.Title" = "Delete Translation"; +"BotPreviews.DeleteTranslationAlert.Text" = "Are you sure you want to delete this translation?"; +"BotPreviews.TranslationFooter.Text" = "This preview will be displayed for all users who have %@ set as their language."; +"BotPreviews.DefaultFooter.Text" = "This preview will be shown by default. You can also add translations into specific languages."; +"BotPreviews.SelectLanguage.Title" = "Add a Translation"; +"BotPreview.ViewContextDelete" = "Delete Preview"; + +"WebBrowser.Download.Confirmation" = "Do you want to download \"%@\"?"; +"WebBrowser.Download.Download" = "Download"; + +"Story.Cover" = "Story Cover"; +"Story.SaveCover" = "Save Cover"; + +"WebBrowser.CopyLink" = "Copy Link"; +"WebBrowser.DeleteBookmark" = "Delete Bookmark"; +"WebBrowser.RemoveRecent" = "Remove from Recent"; + +"Stars.Intro.Transaction.Gift.Title" = "Received Gift"; +"Stars.Intro.Transaction.Gift.UnknownUser" = "Unknown User"; + +"WebApp.PrivacyPolicy" = "Privacy Policy"; + +"Conversation.OpenProfile" = "OPEN PROFILE"; + +"Stars.Intro.GiftStars" = "Gift Stars to Friends"; + +"MediaPicker.CreateSticker" = "Create a sticker from a photo"; diff --git a/WORKSPACE b/WORKSPACE.bzlmod similarity index 59% rename from WORKSPACE rename to WORKSPACE.bzlmod index f8853de18ef..551b9303d03 100644 --- a/WORKSPACE +++ b/WORKSPACE.bzlmod @@ -1,5 +1,12 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file") +# Workaround for telegram build_configuration mechanism +# (passing --override_repository option when running project build script). +# This mechanism does not work when bzlmod is enabled +# ('common --enable_bzlmod' inside .bazelrc file). +# When Telegram itself migrates to Bzlmod, we can remove this line. +http_archive(name = "build_configuration") + http_archive( name = "bazel_skylib", sha256 = "f24ab666394232f834f74d19e2ff142b0af17466ea0c69a3f4c276ee75f6efce", @@ -16,26 +23,6 @@ http_archive( url = "https://github.com/bazel-contrib/bazel_features/releases/download/v1.4.1/bazel_features-v1.4.1.tar.gz", ) -local_repository( - name = "rules_xcodeproj", - path = "build-system/bazel-rules/rules_xcodeproj", -) - -local_repository( - name = "build_bazel_rules_apple", - path = "build-system/bazel-rules/rules_apple", -) - -local_repository( - name = "build_bazel_rules_swift", - path = "build-system/bazel-rules/rules_swift", -) - -local_repository( - name = "build_bazel_apple_support", - path = "build-system/bazel-rules/apple_support", -) - http_file( name = "cmake_tar_gz", urls = ["https://github.com/Kitware/CMake/releases/download/v3.23.1/cmake-3.23.1-macos-universal.tar.gz"], @@ -109,61 +96,4 @@ http_archive( urls = ["https://artifacts.applovin.com/ios/com/applovin/applovin-sdk/applovin-ios-sdk-11.10.1.zip"], build_file = "@//third-party/AppLovin:BUILD", sha256 = "4a0e3aff4634d58307332fdaa2dbad59e4b2534a48c829aa5002d88364685c0d", -) - -# swift_bazel start - -http_archive( - name = "rules_swift_package_manager", - sha256 = "7a75091c5d3132c1a8c24378a6beba469b435daeee9d0870686d77d0c974fbe5", - urls = [ - "https://github.com/cgrindel/rules_swift_package_manager/releases/download/v0.31.1/rules_swift_package_manager.v0.31.1.tar.gz", - ], -) - -load("@rules_swift_package_manager//:deps.bzl", "swift_bazel_dependencies") - -swift_bazel_dependencies() - -load("@cgrindel_bazel_starlib//:deps.bzl", "bazel_starlib_dependencies") - -bazel_starlib_dependencies() - -# MARK: - Gazelle - -# gazelle:repo bazel_gazelle - -load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") -load("@rules_swift_package_manager//:go_deps.bzl", "swift_bazel_go_dependencies") -load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") - -# Declare Go dependencies before calling go_rules_dependencies. -swift_bazel_go_dependencies() - -go_rules_dependencies() - -go_register_toolchains(version = "1.21.1") - -gazelle_dependencies() - -# MARK: - Swift Toolchain - -load( - "@build_bazel_rules_swift//swift:repositories.bzl", - "swift_rules_dependencies", -) -load("//:swift_deps.bzl", "swift_dependencies") - -# gazelle:repository_macro swift_deps.bzl%swift_dependencies -swift_dependencies() - -swift_rules_dependencies() - -load( - "@build_bazel_rules_swift//swift:extras.bzl", - "swift_rules_extra_dependencies", -) - -swift_rules_extra_dependencies() - -#swift_bazel finish +) \ No newline at end of file diff --git a/build-system/Make/BuildConfiguration.py b/build-system/Make/BuildConfiguration.py index 0086e34d46f..061837bb068 100644 --- a/build-system/Make/BuildConfiguration.py +++ b/build-system/Make/BuildConfiguration.py @@ -124,7 +124,7 @@ def decrypt_codesigning_directory_recursively(source_base_path, destination_base source_path = source_base_path + '/' + file_name destination_path = destination_base_path + '/' + file_name if os.path.isfile(source_path): - os.system('openssl aes-256-cbc -md md5 -k "{password}" -in "{source_path}" -out "{destination_path}" -a -d 2>/dev/null'.format( + os.system('ruby build-system/decrypt.rb "{password}" "{source_path}" "{destination_path}"'.format( password=password, source_path=source_path, destination_path=destination_path diff --git a/build-system/bazel-rules/rules_swift b/build-system/bazel-rules/rules_swift index 72347ab6cb6..1aec64c218f 160000 --- a/build-system/bazel-rules/rules_swift +++ b/build-system/bazel-rules/rules_swift @@ -1 +1 @@ -Subproject commit 72347ab6cb613b36039d43a071ac67f63f4de248 +Subproject commit 1aec64c218fc057c2a836e67bd55bc514e0ef8bb diff --git a/build-system/decrypt.rb b/build-system/decrypt.rb new file mode 100644 index 00000000000..2b8561298ae --- /dev/null +++ b/build-system/decrypt.rb @@ -0,0 +1,156 @@ +require 'base64' +require 'openssl' +require 'securerandom' + +class EncryptionV1 + ALGORITHM = 'aes-256-cbc' + + def encrypt(data:, password:, salt:, hash_algorithm: "MD5") + cipher = ::OpenSSL::Cipher.new(ALGORITHM) + cipher.encrypt + + keyivgen(cipher, password, salt, hash_algorithm) + + encrypted_data = cipher.update(data) + encrypted_data << cipher.final + { encrypted_data: encrypted_data } + end + + def decrypt(encrypted_data:, password:, salt:, hash_algorithm: "MD5") + cipher = ::OpenSSL::Cipher.new(ALGORITHM) + cipher.decrypt + + keyivgen(cipher, password, salt, hash_algorithm) + + data = cipher.update(encrypted_data) + data << cipher.final + end + + private + + def keyivgen(cipher, password, salt, hash_algorithm) + cipher.pkcs5_keyivgen(password, salt, 1, hash_algorithm) + end +end + +# The newer encryption mechanism, which features a more secure key and IV generation. +# +# The IV is randomly generated and provided unencrypted. +# The salt should be randomly generated and provided unencrypted (like in the current implementation). +# The key is generated with OpenSSL::KDF::pbkdf2_hmac with properly chosen parameters. +# +# Short explanation about salt and IV: https://stackoverflow.com/a/1950674/6324550 +class EncryptionV2 + ALGORITHM = 'aes-256-gcm' + + def encrypt(data:, password:, salt:) + cipher = ::OpenSSL::Cipher.new(ALGORITHM) + cipher.encrypt + + keyivgen(cipher, password, salt) + + encrypted_data = cipher.update(data) + encrypted_data << cipher.final + + auth_tag = cipher.auth_tag + + { encrypted_data: encrypted_data, auth_tag: auth_tag } + end + + def decrypt(encrypted_data:, password:, salt:, auth_tag:) + cipher = ::OpenSSL::Cipher.new(ALGORITHM) + cipher.decrypt + + keyivgen(cipher, password, salt) + + cipher.auth_tag = auth_tag + + data = cipher.update(encrypted_data) + data << cipher.final + end + + private + + def keyivgen(cipher, password, salt) + keyIv = ::OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: 10_000, length: 32 + 12 + 24, hash: "sha256") + key = keyIv[0..31] + iv = keyIv[32..43] + auth_data = keyIv[44..-1] + cipher.key = key + cipher.iv = iv + cipher.auth_data = auth_data + end +end + +class MatchDataEncryption + V1_PREFIX = "Salted__" + V2_PREFIX = "match_encrypted_v2__" + + def encrypt(data:, password:, version: 2) + salt = SecureRandom.random_bytes(8) + if version == 2 + e = EncryptionV2.new + encryption = e.encrypt(data: data, password: password, salt: salt) + encrypted_data = V2_PREFIX + salt + encryption[:auth_tag] + encryption[:encrypted_data] + else + e = EncryptionV1.new + encryption = e.encrypt(data: data, password: password, salt: salt) + encrypted_data = V1_PREFIX + salt + encryption[:encrypted_data] + end + Base64.encode64(encrypted_data) + end + + def decrypt(base64encoded_encrypted:, password:) + stored_data = Base64.decode64(base64encoded_encrypted) + if stored_data.start_with?(V2_PREFIX) + salt = stored_data[20..27] + auth_tag = stored_data[28..43] + data_to_decrypt = stored_data[44..-1] + + e = EncryptionV2.new + e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt, auth_tag: auth_tag) + else + salt = stored_data[8..15] + data_to_decrypt = stored_data[16..-1] + e = EncryptionV1.new + begin + # Note that we are not guaranteed to catch the decryption errors here if the password or the hash is wrong + # as there's no integrity checks. + # see https://github.com/fastlane/fastlane/issues/21663 + e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt) + # With the wrong hash_algorithm, there's here 0.4% chance that the decryption failure will go undetected + rescue => _ex + # With a wrong password, there's a 0.4% chance it will decrypt garbage and not fail + fallback_hash_algorithm = "SHA256" + e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt, hash_algorithm: fallback_hash_algorithm) + end + end + end +end + + +class MatchFileEncryption + def encrypt(file_path:, password:, output_path: nil) + output_path = file_path unless output_path + data_to_encrypt = File.binread(file_path) + e = MatchDataEncryption.new + data = e.encrypt(data: data_to_encrypt, password: password) + File.write(output_path, data) + end + + def decrypt(file_path:, password:, output_path: nil) + output_path = file_path unless output_path + content = File.read(file_path) + e = MatchDataEncryption.new + decrypted_data = e.decrypt(base64encoded_encrypted: content, password: password) + File.binwrite(output_path, decrypted_data) + end +end + + +if ARGV.length != 3 + print 'Invalid command line' +else + dec = MatchFileEncryption.new + dec.decrypt(file_path: ARGV[1], password: ARGV[0], output_path: ARGV[2]) +end diff --git a/build-system/tulsi b/build-system/tulsi deleted file mode 160000 index a0bf60e1645..00000000000 --- a/build-system/tulsi +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a0bf60e1645869c6452c9f3b128362d433764f19 diff --git a/ci/fastlane/Fastfile b/ci/fastlane/Fastfile index b87f35b4648..b59b5b32c7a 100644 --- a/ci/fastlane/Fastfile +++ b/ci/fastlane/Fastfile @@ -117,38 +117,6 @@ lane :build do |options| delete_keychain_if_exists(name: KEYCHAIN_NAME) end -lane :update_nice_localization do |options| - translations_path = options[:translations] - project_path = options[:project] - - FileUtils.rm_rf(Dir.glob("#{project_path}/Telegram/Telegram-iOS/*/NiceLocalizable.strings")) - FileUtils.rm_rf(Dir.glob("#{project_path}/Telegram/Telegram-iOS/*/NicegramLocalizable.strings")) - FileUtils.cp_r("#{translations_path}/master/Telegram-iOS", "#{project_path}/Telegram") - FileUtils.remove_dir(translations_path) -end - -lane :upload_nice_localization do |options| - destination_path = options[:dest] - project_path = options[:project] - - folder_path = "#{destination_path}/NiceLocalizable" - FileUtils.rm_rf(folder_path) - FileUtils.mkdir(folder_path) - - index = 1 - Dir.glob("#{project_path}/Telegram/Telegram-iOS/*/NiceLocalizable.strings") do |filename| - FileUtils.cp(filename, "#{folder_path}/#{index}.strings") - index += 1 - end - - zip( - path: folder_path, - output_path: "#{destination_path}/NiceLocalizable.zip" - ) - - FileUtils.rm_rf(folder_path) -end - lane :resolve_telegram_configuration do |options| is_appstore_build = options[:is_appstore_build] || true @@ -181,8 +149,6 @@ lane :build_bazel do |options| git_codesigning_repository = options[:git_codesigning_repository] git_codesigning_type = options[:git_codesigning_type] - update_spm_pkgs() - artifacts_path = "#{BUILD_WORKING_DIR}/artifacts" FileUtils.rm_rf(artifacts_path) FileUtils.mkdir(artifacts_path) @@ -234,8 +200,6 @@ lane :generate_project do |options| configuration_path = resolve_telegram_configuration() - update_spm_pkgs() - sh "cd #{SOURCE_PATH} && python3 build-system/Make/Make.py \ --cacheDir=#{BAZEL_LOCAL_CACHE} \ generateProject \ @@ -282,11 +246,6 @@ lane :update_remote_config_defaults do |options| File.delete(credentials_file_path) end -lane :update_spm_pkgs do |options| - bazel_path = sh("cd #{SOURCE_PATH} && echo $(python3 #{SOURCE_PATH}/build-system/Make/LocateBazel.py)") - sh('#{bazel_path} bazel run //:swift_update_pkgs') -end - lane :nicegram_match do |options| type = options[:type] diff --git a/ci/fastlane/README.md b/ci/fastlane/README.md index 69be736cdc8..08b90003e0f 100644 --- a/ci/fastlane/README.md +++ b/ci/fastlane/README.md @@ -45,22 +45,6 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do -### update_nice_localization - -```sh -[bundle exec] fastlane update_nice_localization -``` - - - -### upload_nice_localization - -```sh -[bundle exec] fastlane upload_nice_localization -``` - - - ### resolve_telegram_configuration ```sh @@ -101,14 +85,6 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do -### update_spm_pkgs - -```sh -[bundle exec] fastlane update_spm_pkgs -``` - - - ### nicegram_match ```sh diff --git a/ci/update-spm.sh b/ci/update-spm.sh index 6fb542db93b..851366dadb9 100755 --- a/ci/update-spm.sh +++ b/ci/update-spm.sh @@ -1 +1 @@ -bazel run //:swift_update_pkgs_to_latest +swift package update diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 98eccba52d9..2567bd2fdad 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -305,6 +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 chatFolder(slug: String) case story(peerId: PeerId, id: Int32) case boost(peerId: PeerId?, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?) @@ -614,106 +615,11 @@ public enum ContactListActionItemIcon : Equatable { } } -public struct ContactListAdditionalOption: Equatable { - public let title: String - public let icon: ContactListActionItemIcon - public let action: () -> Void - public let clearHighlightAutomatically: Bool - - public init(title: String, icon: ContactListActionItemIcon, action: @escaping () -> Void, clearHighlightAutomatically: Bool = false) { - self.title = title - self.icon = icon - self.action = action - self.clearHighlightAutomatically = clearHighlightAutomatically - } - - public static func ==(lhs: ContactListAdditionalOption, rhs: ContactListAdditionalOption) -> Bool { - return lhs.title == rhs.title && lhs.icon == rhs.icon - } -} - -public enum ContactListPeerId: Hashable { - case peer(PeerId) - case deviceContact(DeviceContactStableId) -} - -public enum ContactListAction: Equatable { - case generic - case voiceCall - case videoCall - case more -} - -public enum ContactListPeer: Equatable { - case peer(peer: Peer, isGlobal: Bool, participantCount: Int32?) - case deviceContact(DeviceContactStableId, DeviceContactBasicData) - - public var id: ContactListPeerId { - switch self { - case let .peer(peer, _, _): - return .peer(peer.id) - case let .deviceContact(id, _): - return .deviceContact(id) - } - } - - public var indexName: PeerIndexNameRepresentation { - switch self { - case let .peer(peer, _, _): - return peer.indexName - case let .deviceContact(_, contact): - return .personName(first: contact.firstName, last: contact.lastName, addressNames: [], phoneNumber: "") - } - } - - public static func ==(lhs: ContactListPeer, rhs: ContactListPeer) -> Bool { - switch lhs { - case let .peer(lhsPeer, lhsIsGlobal, lhsParticipantCount): - if case let .peer(rhsPeer, rhsIsGlobal, rhsParticipantCount) = rhs, lhsPeer.isEqual(rhsPeer), lhsIsGlobal == rhsIsGlobal, lhsParticipantCount == rhsParticipantCount { - return true - } else { - return false - } - case let .deviceContact(id, contact): - if case .deviceContact(id, contact) = rhs { - return true - } else { - return false - } - } - } -} - -public final class ContactSelectionControllerParams { - public let context: AccountContext - public let updatedPresentationData: (initial: PresentationData, signal: Signal)? - public let autoDismiss: Bool - public let title: (PresentationStrings) -> String - public let options: [ContactListAdditionalOption] - public let displayDeviceContacts: Bool - public let displayCallIcons: Bool - public let multipleSelection: Bool - public let requirePhoneNumbers: Bool - public let confirmation: (ContactListPeer) -> Signal - - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: [ContactListAdditionalOption] = [], displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, multipleSelection: Bool = false, requirePhoneNumbers: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal = { _ in .single(true) }) { - self.context = context - self.updatedPresentationData = updatedPresentationData - self.autoDismiss = autoDismiss - self.title = title - self.options = options - self.displayDeviceContacts = displayDeviceContacts - self.displayCallIcons = displayCallIcons - self.multipleSelection = multipleSelection - self.requirePhoneNumbers = requirePhoneNumbers - self.confirmation = confirmation - } -} - public enum ChatListSearchFilter: Equatable { case chats case topics case channels + case apps case media case downloads case links @@ -731,18 +637,20 @@ public enum ChatListSearchFilter: Equatable { return 1 case .channels: return 2 - case .media: + case .apps: return 3 - case .downloads: + case .media: return 4 - case .links: + case .downloads: return 5 - case .files: + case .links: return 6 - case .music: + case .files: return 7 - case .voice: + case .music: return 8 + case .voice: + return 9 case let .peer(peerId, _, _, _): return peerId.id._internalGetInt64Value() case let .date(_, date, _): @@ -806,15 +714,18 @@ public struct StoryCameraTransitionOut { public weak var destinationView: UIView? public let destinationRect: CGRect public let destinationCornerRadius: CGFloat + public let completion: (() -> Void)? public init( destinationView: UIView, destinationRect: CGRect, - destinationCornerRadius: CGFloat + destinationCornerRadius: CGFloat, + completion: (() -> Void)? = nil ) { self.destinationView = destinationView self.destinationRect = destinationRect self.destinationCornerRadius = destinationCornerRadius + self.completion = completion } } @@ -849,12 +760,12 @@ public class MediaEditorTransitionOutExternalState { } public protocol MediaEditorScreenResult { - + var target: Stories.PendingTarget { get } } public protocol TelegramRootControllerInterface: NavigationController { @discardableResult - func openStoryCamera(customTarget: EnginePeer.Id?, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? + func openStoryCamera(customTarget: Stories.PendingTarget?, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? func proceedWithStoryUpload(target: Stories.PendingTarget, result: MediaEditorScreenResult, existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) func getContactsController() -> ViewController? @@ -910,6 +821,29 @@ public struct ChatControllerParams { } } +public enum ChatOpenWebViewSource: Equatable { + case generic + case menu + case inline(bot: EnginePeer) +} + +public final class BotPreviewEditorTransitionOut { + public weak var destinationView: UIView? + public let destinationRect: CGRect + public let destinationCornerRadius: CGFloat + public let completion: (() -> Void)? + + public init(destinationView: UIView?, destinationRect: CGRect, destinationCornerRadius: CGFloat, completion: (() -> Void)?) { + self.destinationView = destinationView + self.destinationRect = destinationRect + self.destinationCornerRadius = destinationCornerRadius + self.completion = completion + } +} + +public protocol MiniAppListScreenInitialData: AnyObject { +} + public protocol SharedAccountContext: AnyObject { var sharedContainerPath: String { get } var basePath: String { get } @@ -979,7 +913,7 @@ public protocol SharedAccountContext: AnyObject { selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode ) -> ChatHistoryListNode - func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem + func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: ((UIView?, CGPoint?) -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader func makeChatMessageAvatarHeaderItem(context: AccountContext, timestamp: Int32, peer: Peer, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader func makePeerSharedMediaController(context: AccountContext, peerId: PeerId) -> ViewController? @@ -1036,7 +970,8 @@ public protocol SharedAccountContext: AnyObject { func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void) func openImagePicker(context: AccountContext, completion: @escaping (UIImage) -> Void, present: @escaping (ViewController) -> Void) func openAddPeerMembers(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, parentController: ViewController, groupPeer: Peer, selectAddMemberDisposable: MetaDisposable, addMemberDisposable: MetaDisposable) - func openChatInstantPage(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?, navigationController: NavigationController) + func makeInstantPageController(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController? + func makeInstantPageController(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController @@ -1046,7 +981,7 @@ public protocol SharedAccountContext: AnyObject { func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController - func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (() -> Void)?) -> ViewController + func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController @@ -1054,10 +989,14 @@ public protocol SharedAccountContext: AnyObject { func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController + func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: (url: String, name: String?)?, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController + + func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, transitionOut: @escaping () -> BotPreviewEditorTransitionOut?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController + func makeStickerEditorScreen(context: AccountContext, source: Any?, intro: Bool, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController - func makeStoryMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController + func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController func makeStickerPickerScreen(context: AccountContext, inputData: Promise, completion: @escaping (FileMediaReference) -> Void) -> ViewController @@ -1070,13 +1009,19 @@ public protocol SharedAccountContext: AnyObject { func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController - func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int64?, completion: @escaping (Int64) -> Void) -> 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 makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> 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 + func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController + + func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal + func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController + + func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) func makeDebugSettingsController(context: AccountContext?) -> ViewController? diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index f79b6ff0ff9..c2bfca00d4c 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -292,12 +292,12 @@ public struct ChatControllerInitialAttachBotStart { } public struct ChatControllerInitialBotAppStart { - public let botApp: BotApp + public let botApp: BotApp? public let payload: String? public let justInstalled: Bool public let compact: Bool - public init(botApp: BotApp, payload: String?, justInstalled: Bool, compact: Bool) { + public init(botApp: BotApp?, payload: String?, justInstalled: Bool, compact: Bool) { self.botApp = botApp self.payload = payload self.justInstalled = justInstalled @@ -774,7 +774,7 @@ public enum ChatControllerSubject: Equatable { } } - case message(id: MessageSubject, highlight: MessageHighlight?, timecode: Double?) + case message(id: MessageSubject, highlight: MessageHighlight?, timecode: Double?, setupReply: Bool) case scheduledMessages case pinnedMessages(id: EngineMessage.Id?) case messageOptions(peerIds: [EnginePeer.Id], ids: [EngineMessage.Id], info: MessageOptionsInfo) @@ -782,8 +782,8 @@ public enum ChatControllerSubject: Equatable { public static func ==(lhs: ChatControllerSubject, rhs: ChatControllerSubject) -> Bool { switch lhs { - case let .message(lhsId, lhsHighlight, lhsTimecode): - if case let .message(rhsId, rhsHighlight, rhsTimecode) = rhs, lhsId == rhsId && lhsHighlight == rhsHighlight && lhsTimecode == rhsTimecode { + case let .message(lhsId, lhsHighlight, lhsTimecode, lhsSetupReply): + if case let .message(rhsId, rhsHighlight, rhsTimecode, rhsSetupReply) = rhs, lhsId == rhsId && lhsHighlight == rhsHighlight && lhsTimecode == rhsTimecode && lhsSetupReply == rhsSetupReply { return true } else { return false @@ -962,6 +962,7 @@ public protocol PeerInfoScreen: ViewController { func openBirthdaySetup() func toggleStorySelection(ids: [Int32], isSelected: Bool) + func togglePaneIsReordering(isReordering: Bool) func cancelItemSelection() } @@ -1014,7 +1015,7 @@ public protocol ChatController: ViewController { var canReadHistory: ValuePromise { get } var parentController: ViewController? { get set } var customNavigationController: NavigationController? { get set } - + var purposefulAction: (() -> Void)? { get set } var stateUpdated: ((ContainedViewLayoutTransition) -> Void)? { get set } @@ -1197,4 +1198,6 @@ public protocol ChatHistoryListNode: ListView { func scrollToEndOfHistory() func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) func messageInCurrentHistoryView(_ id: MessageId) -> Message? + + var contentPositionChanged: (ListViewVisibleContentOffset) -> Void { get set } } diff --git a/submodules/AccountContext/Sources/ChatHistoryLocation.swift b/submodules/AccountContext/Sources/ChatHistoryLocation.swift index 5d84475d93c..261ef74d6a5 100644 --- a/submodules/AccountContext/Sources/ChatHistoryLocation.swift +++ b/submodules/AccountContext/Sources/ChatHistoryLocation.swift @@ -20,10 +20,12 @@ public struct MessageHistoryScrollToSubject: Equatable { public var index: MessageHistoryAnchorIndex public var quote: Quote? + public var setupReply: Bool - public init(index: MessageHistoryAnchorIndex, quote: Quote?) { + public init(index: MessageHistoryAnchorIndex, quote: Quote?, setupReply: Bool = false) { self.index = index self.quote = quote + self.setupReply = setupReply } } @@ -49,9 +51,9 @@ public struct MessageHistoryInitialSearchSubject: Equatable { public enum ChatHistoryLocation: Equatable { case Initial(count: Int) - case InitialSearch(subject: MessageHistoryInitialSearchSubject, count: Int, highlight: Bool) + case InitialSearch(subject: MessageHistoryInitialSearchSubject, count: Int, highlight: Bool, setupReply: Bool) case Navigation(index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, count: Int, highlight: Bool) - case Scroll(subject: MessageHistoryScrollToSubject, anchorIndex: MessageHistoryAnchorIndex, sourceIndex: MessageHistoryAnchorIndex, scrollPosition: ListViewScrollPosition, animated: Bool, highlight: Bool) + case Scroll(subject: MessageHistoryScrollToSubject, anchorIndex: MessageHistoryAnchorIndex, sourceIndex: MessageHistoryAnchorIndex, scrollPosition: ListViewScrollPosition, animated: Bool, highlight: Bool, setupReply: Bool) } public struct ChatHistoryLocationInput: Equatable { diff --git a/submodules/AccountContext/Sources/ContactMultiselectionController.swift b/submodules/AccountContext/Sources/ContactMultiselectionController.swift index 8bbc7dd68df..88192e710ee 100644 --- a/submodules/AccountContext/Sources/ContactMultiselectionController.swift +++ b/submodules/AccountContext/Sources/ContactMultiselectionController.swift @@ -78,13 +78,14 @@ public enum ContactMultiselectionControllerMode { case peerSelection(searchChatList: Bool, searchGroups: Bool, searchChannels: Bool) case channelCreation case chatSelection(ChatSelection) - case premiumGifting(birthdays: [EnginePeer.Id: TelegramBirthday]?, selectToday: Bool) + case premiumGifting(birthdays: [EnginePeer.Id: TelegramBirthday]?, selectToday: Bool, hasActions: Bool) case requestedUsersSelection } public enum ContactListFilter { case excludeWithoutPhoneNumbers case excludeSelf + case excludeBots case exclude([EnginePeer.Id]) case disable([EnginePeer.Id]) } diff --git a/submodules/AccountContext/Sources/ContactSelectionController.swift b/submodules/AccountContext/Sources/ContactSelectionController.swift index 19d4c5c60ab..c16d2956056 100644 --- a/submodules/AccountContext/Sources/ContactSelectionController.swift +++ b/submodules/AccountContext/Sources/ContactSelectionController.swift @@ -1,6 +1,9 @@ import Foundation import Display import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData public protocol ContactSelectionController: ViewController { var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?, ChatSendMessageActionSheetController.SendParameters?)?, NoError> { get } @@ -10,3 +13,106 @@ public protocol ContactSelectionController: ViewController { func dismissSearch() } + +public enum ContactSelectionControllerMode { + case generic + case starsGifting(birthdays: [EnginePeer.Id: TelegramBirthday]?, hasActions: Bool) +} + +public struct ContactListAdditionalOption: Equatable { + public let title: String + public let icon: ContactListActionItemIcon + public let action: () -> Void + public let clearHighlightAutomatically: Bool + + public init(title: String, icon: ContactListActionItemIcon, action: @escaping () -> Void, clearHighlightAutomatically: Bool = false) { + self.title = title + self.icon = icon + self.action = action + self.clearHighlightAutomatically = clearHighlightAutomatically + } + + public static func ==(lhs: ContactListAdditionalOption, rhs: ContactListAdditionalOption) -> Bool { + return lhs.title == rhs.title && lhs.icon == rhs.icon + } +} + +public enum ContactListPeerId: Hashable { + case peer(PeerId) + case deviceContact(DeviceContactStableId) +} + +public enum ContactListAction: Equatable { + case generic + case voiceCall + case videoCall + case more +} + +public enum ContactListPeer: Equatable { + case peer(peer: Peer, isGlobal: Bool, participantCount: Int32?) + case deviceContact(DeviceContactStableId, DeviceContactBasicData) + + public var id: ContactListPeerId { + switch self { + case let .peer(peer, _, _): + return .peer(peer.id) + case let .deviceContact(id, _): + return .deviceContact(id) + } + } + + public var indexName: PeerIndexNameRepresentation { + switch self { + case let .peer(peer, _, _): + return peer.indexName + case let .deviceContact(_, contact): + return .personName(first: contact.firstName, last: contact.lastName, addressNames: [], phoneNumber: "") + } + } + + public static func ==(lhs: ContactListPeer, rhs: ContactListPeer) -> Bool { + switch lhs { + case let .peer(lhsPeer, lhsIsGlobal, lhsParticipantCount): + if case let .peer(rhsPeer, rhsIsGlobal, rhsParticipantCount) = rhs, lhsPeer.isEqual(rhsPeer), lhsIsGlobal == rhsIsGlobal, lhsParticipantCount == rhsParticipantCount { + return true + } else { + return false + } + case let .deviceContact(id, contact): + if case .deviceContact(id, contact) = rhs { + return true + } else { + return false + } + } + } +} + +public final class ContactSelectionControllerParams { + public let context: AccountContext + public let updatedPresentationData: (initial: PresentationData, signal: Signal)? + public let mode: ContactSelectionControllerMode + public let autoDismiss: Bool + public let title: (PresentationStrings) -> String + public let options: Signal<[ContactListAdditionalOption], NoError> + public let displayDeviceContacts: Bool + public let displayCallIcons: Bool + public let multipleSelection: Bool + public let requirePhoneNumbers: Bool + public let confirmation: (ContactListPeer) -> Signal + + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: ContactSelectionControllerMode = .generic, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: Signal<[ContactListAdditionalOption], NoError> = .single([]), displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, multipleSelection: Bool = false, requirePhoneNumbers: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal = { _ in .single(true) }) { + self.context = context + self.updatedPresentationData = updatedPresentationData + self.mode = mode + self.autoDismiss = autoDismiss + self.title = title + self.options = options + self.displayDeviceContacts = displayDeviceContacts + self.displayCallIcons = displayCallIcons + self.multipleSelection = multipleSelection + self.requirePhoneNumbers = requirePhoneNumbers + self.confirmation = confirmation + } +} diff --git a/submodules/AccountContext/Sources/IsMediaStreamable.swift b/submodules/AccountContext/Sources/IsMediaStreamable.swift index a562f0a32c8..650911e79ae 100644 --- a/submodules/AccountContext/Sources/IsMediaStreamable.swift +++ b/submodules/AccountContext/Sources/IsMediaStreamable.swift @@ -18,7 +18,7 @@ public func isMediaStreamable(message: Message, media: TelegramMediaFile) -> Boo return false } for attribute in media.attributes { - if case let .Video(_, _, flags, _) = attribute { + if case let .Video(_, _, flags, _, _) = attribute { if flags.contains(.supportsStreaming) { return true } @@ -41,7 +41,7 @@ public func isMediaStreamable(media: TelegramMediaFile) -> Bool { return false } for attribute in media.attributes { - if case let .Video(_, _, flags, _) = attribute { + if case let .Video(_, _, flags, _, _) = attribute { if flags.contains(.supportsStreaming) { return true } diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 46d61341115..183c5aeea7a 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -49,6 +49,7 @@ public enum PremiumGiftSource: Equatable { case attachMenu case settings([EnginePeer.Id: TelegramBirthday]?) case chatList([EnginePeer.Id: TelegramBirthday]?) + case stars([EnginePeer.Id: TelegramBirthday]?) case channelBoost case deeplink(String?) } @@ -121,6 +122,14 @@ public enum BoostSubject: Equatable { case noAds } +public enum StarsPurchasePurpose: Equatable { + case generic(requiredStars: Int64?) + case transfer(peerId: EnginePeer.Id, requiredStars: Int64) + case subscription(peerId: EnginePeer.Id, requiredStars: Int64, renew: Bool) + case gift(peerId: EnginePeer.Id) + case unlockMedia(requiredStars: Int64) +} + public struct PremiumConfiguration { public static var defaultValue: PremiumConfiguration { return PremiumConfiguration( @@ -130,6 +139,7 @@ public struct PremiumConfiguration { showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, + starsGiftsPurchaseAvailable: false, boostsPerGiftCount: 3, audioTransciptionTrialMaxDuration: 300, audioTransciptionTrialCount: 2, @@ -156,6 +166,7 @@ public struct PremiumConfiguration { public let showPremiumGiftInAttachMenu: Bool public let showPremiumGiftInTextField: Bool public let giveawayGiftsPurchaseAvailable: Bool + public let starsGiftsPurchaseAvailable: Bool public let boostsPerGiftCount: Int32 public let audioTransciptionTrialMaxDuration: Int32 public let audioTransciptionTrialCount: Int32 @@ -181,6 +192,7 @@ public struct PremiumConfiguration { showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, + starsGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32, audioTransciptionTrialMaxDuration: Int32, audioTransciptionTrialCount: Int32, @@ -205,6 +217,7 @@ public struct PremiumConfiguration { self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu self.showPremiumGiftInTextField = showPremiumGiftInTextField self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable + self.starsGiftsPurchaseAvailable = starsGiftsPurchaseAvailable self.boostsPerGiftCount = boostsPerGiftCount self.audioTransciptionTrialMaxDuration = audioTransciptionTrialMaxDuration self.audioTransciptionTrialCount = audioTransciptionTrialCount @@ -237,6 +250,7 @@ public struct PremiumConfiguration { showPremiumGiftInAttachMenu: data["premium_gift_attach_menu_icon"] as? Bool ?? defaultValue.showPremiumGiftInAttachMenu, showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? defaultValue.showPremiumGiftInTextField, giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? defaultValue.giveawayGiftsPurchaseAvailable, + starsGiftsPurchaseAvailable: data["stars_gifts_enabled"] as? Bool ?? defaultValue.starsGiftsPurchaseAvailable, boostsPerGiftCount: get(data["boosts_per_sent_gift"]) ?? defaultValue.boostsPerGiftCount, audioTransciptionTrialMaxDuration: get(data["transcribe_audio_trial_duration_max"]) ?? defaultValue.audioTransciptionTrialMaxDuration, audioTransciptionTrialCount: get(data["transcribe_audio_trial_weekly_number"]) ?? defaultValue.audioTransciptionTrialCount, diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index 7e3ccaeb2a8..d9eb6947cee 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -653,7 +653,7 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke strongSelf.frameUpdated(frame.index, frame.totalFrames) strongSelf.currentFrameIndex = frame.index - strongSelf.currentFrameCount = frame.totalFrames; + strongSelf.currentFrameCount = frame.totalFrames strongSelf.currentFrameRate = frameRate if frame.isLastFrame { diff --git a/submodules/AttachmentUI/Sources/AttachmentContainer.swift b/submodules/AttachmentUI/Sources/AttachmentContainer.swift index 61549339325..1ddaf947db4 100644 --- a/submodules/AttachmentUI/Sources/AttachmentContainer.swift +++ b/submodules/AttachmentUI/Sources/AttachmentContainer.swift @@ -71,6 +71,7 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { var isPanningUpdated: (Bool) -> Void = { _ in } var isExpandedUpdated: (Bool) -> Void = { _ in } var isPanGestureEnabled: (() -> Bool)? + var isInnerPanGestureEnabled: (() -> Bool)? var onExpandAnimationCompleted: () -> Void = {} init(isFullSize: Bool) { @@ -146,6 +147,23 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { return false } } + if let isInnerPanGestureEnabled = self.isInnerPanGestureEnabled, !isInnerPanGestureEnabled() { + func findWebViewAncestor(view: UIView?) -> WKWebView? { + guard let view else { + return nil + } + if let view = view as? WKWebView { + return view + } else if view != self.view { + return findWebViewAncestor(view: view.superview) + } else { + return nil + } + } + if let otherView = self.hitTest(gestureRecognizer.location(in: self.view), with: nil), let _ = findWebViewAncestor(view: otherView) { + return false + } + } } return true } @@ -163,23 +181,6 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { } return true } - if gestureRecognizer is UIPanGestureRecognizer { - func findWebViewAncestor(view: UIView?) -> WKWebView? { - guard let view else { - return nil - } - if let view = view as? WKWebView { - return view - } else if view != self.view { - return findWebViewAncestor(view: view.superview) - } else { - return nil - } - } - if let otherView = otherGestureRecognizer.view, let _ = findWebViewAncestor(view: otherView) { - return true - } - } if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UILongPressGestureRecognizer { return true } @@ -197,7 +198,7 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)? @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { - guard let (layout, controllers, coveredByModalTransition) = self.validLayout else { + guard let (layout, controllers, coveredByModalTransition) = self.validLayout, let lastController = controllers.last else { return } @@ -271,9 +272,13 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { } if !self.isExpanded || self.isFullSize, translation > 40.0, let shouldCancelPanGesture = self.shouldCancelPanGesture, shouldCancelPanGesture() { - self.cancelPanGesture() - self.requestDismiss?() - return + if lastController.isMinimizable { + + } else { + self.cancelPanGesture() + self.requestDismiss?() + return + } } var bounds = self.bounds @@ -323,7 +328,11 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { var ignoreDismiss = false if let shouldCancelPanGesture = self.shouldCancelPanGesture, shouldCancelPanGesture() { - ignoreDismiss = true + if lastController.isMinimizable { + + } else { + ignoreDismiss = true + } } var minimizing = false diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index fb11cd182b1..60646efeb7a 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -111,7 +111,7 @@ public enum AttachmentButtonType: Equatable { } } -public protocol AttachmentContainable: ViewController { +public protocol AttachmentContainable: ViewController, MinimizableController { var requestAttachmentMenuExpansion: () -> Void { get set } var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void { get set } var parentController: () -> ViewController? { get set } @@ -121,6 +121,7 @@ public protocol AttachmentContainable: ViewController { var isContainerPanning: () -> Bool { get set } var isContainerExpanded: () -> Bool { get set } var isPanGestureEnabled: (() -> Bool)? { get } + var isInnerPanGestureEnabled: (() -> Bool)? { get } var mediaPickerContext: AttachmentMediaPickerContext? { get } var getCurrentSendMessageContextMediaPreview: (() -> ChatSendMessageContextScreenMediaPreview?)? { get } @@ -160,10 +161,30 @@ public extension AttachmentContainable { completion() } + var minimizedBounds: CGRect? { + return nil + } + + var minimizedTopEdgeOffset: CGFloat? { + return nil + } + + var minimizedIcon: UIImage? { + return nil + } + + var minimizedProgress: Float? { + return nil + } + var isPanGestureEnabled: (() -> Bool)? { return nil } + var isInnerPanGestureEnabled: (() -> Bool)? { + return nil + } + var getCurrentSendMessageContextMediaPreview: (() -> ChatSendMessageContextScreenMediaPreview?)? { return nil } @@ -188,6 +209,10 @@ public protocol AttachmentMediaPickerContext { var captionIsAboveMedia: Signal { get } func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void + var canMakePaidContent: Bool { get } + var price: Int64? { get } + func setPrice(_ price: Int64) -> Void + var loadingProgress: Signal { get } var mainButtonState: Signal { get } @@ -198,6 +223,58 @@ public protocol AttachmentMediaPickerContext { func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) } +public extension AttachmentMediaPickerContext { + var selectionCount: Signal { + return .single(0) + } + + var caption: Signal { + return .single(nil) + } + + var captionIsAboveMedia: Signal { + return .single(false) + } + + var hasCaption: Bool { + return false + } + + func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { + } + + var canMakePaidContent: Bool { + return false + } + + var price: Int64? { + return nil + } + + func setPrice(_ price: Int64) -> Void { + } + + var loadingProgress: Signal { + return .single(nil) + } + + var mainButtonState: Signal { + return .single(nil) + } + + func setCaption(_ caption: NSAttributedString) { + } + + func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { + } + + func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { + } + + func mainButtonAction() { + } +} + private func generateShadowImage() -> UIImage? { return generateImage(CGSize(width: 140.0, height: 140.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -231,7 +308,7 @@ private func generateMaskImage() -> UIImage? { })?.stretchableImage(withLeftCapWidth: 195, topCapHeight: 110) } -public class AttachmentController: ViewController { +public class AttachmentController: ViewController, MinimizableController { private let context: AccountContext private let updatedPresentationData: (initial: PresentationData, signal: Signal)? private let chatLocation: ChatLocation? @@ -260,6 +337,12 @@ public class AttachmentController: ViewController { override public var ready: Promise { return self._ready } + + public private(set) var minimizedTopEdgeOffset: CGFloat? + public private(set) var minimizedBounds: CGRect? + public var minimizedIcon: UIImage? { + return self.mainController.minimizedIcon + } private final class Node: ASDisplayNode { private weak var controller: AttachmentController? @@ -429,8 +512,18 @@ public class AttachmentController: ViewController { if let isPanGestureEnabled = currentController.isPanGestureEnabled { return isPanGestureEnabled() } else { -// MARK: Nicegram disabled pan gesture close swipe on inapp application content - return isPanGestureEnabled + return true + } + } + + self.container.isInnerPanGestureEnabled = { [weak self] in + guard let self, let currentController = self.currentControllers.last else { + return true + } + if let isInnerPanGestureEnabled = currentController.isInnerPanGestureEnabled { + return isInnerPanGestureEnabled() + } else { + return true } } @@ -583,11 +676,7 @@ public class AttachmentController: ViewController { return } navigationController.minimizeViewController(controller, damping: damping, velocity: initialVelocity, beforeMaximize: { navigationController, completion in - if let controller = controller.mainController as? AttachmentContainable { - controller.beforeMaximize(navigationController: navigationController, completion: completion) - } else { - completion() - } + controller.mainController.beforeMaximize(navigationController: navigationController, completion: completion) }, setupContainer: { [weak self] current in let minimizedContainer: MinimizedContainerImpl? if let current = current as? MinimizedContainerImpl { @@ -624,7 +713,7 @@ public class AttachmentController: ViewController { } if case .ended = recognizer.state { if let lastController = self.currentControllers.last { - if let controller = self.controller, controller.shouldMinimizeOnSwipe?(self.currentType) == true { + if let controller = self.controller, let layout = self.validLayout, !layout.metrics.isTablet, controller.shouldMinimizeOnSwipe?(self.currentType) == true { self.minimize() return } @@ -854,9 +943,6 @@ public class AttachmentController: ViewController { self.currentControllers.last?.scrollToTop?() } -// MARK: Nicegram disabled pan gesture close swipe on inapp application content - var isPanGestureEnabled = true - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let controller = self.controller, controller.isInteractionDisabled() { return self.view @@ -865,14 +951,10 @@ public class AttachmentController: ViewController { if result == self.wrapperNode.view { return self.dim.view } -// MARK: Nicegram disabled pan gesture close swipe on inapp application content - isPanGestureEnabled = (result?.frame.width ?? 0) == container.frame.width && - controller?.initialButton == .standalone ? false : true - return result } } - + private var isUpdatingContainer = false private var switchingController = false @@ -1101,6 +1183,8 @@ public class AttachmentController: ViewController { self.blocksBackgroundWhenInOverlay = true self.acceptsFocusWhenInOverlay = true + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil) + self.scrollToTop = { [weak self] in if let strongSelf = self { strongSelf.node.scrollToTop() @@ -1186,12 +1270,20 @@ public class AttachmentController: ViewController { return false } - public override var isMinimized: Bool { + public var isMinimized: Bool = false { didSet { self.mainController.isMinimized = self.isMinimized } } + public var isMinimizable: Bool { + return self.mainController.isMinimizable + } + + public func shouldDismissImmediately() -> Bool { + return self.mainController.shouldDismissImmediately() + } + private var validLayout: ContainerViewLayout? override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -1207,7 +1299,7 @@ public class AttachmentController: ViewController { self.node.containerLayoutUpdated(layout, transition: transition) } - public var mainController: ViewController { + public var mainController: AttachmentContainable { return self.node.currentControllers.first! } @@ -1273,4 +1365,13 @@ public class AttachmentController: ViewController { }) return disposableSet } + + public func makeContentSnapshotView() -> UIView? { + let snapshotView = self.view.snapshotView(afterScreenUpdates: false) + if let contentSnapshotView = self.mainController.makeContentSnapshotView() { + contentSnapshotView.frame = contentSnapshotView.frame.offsetBy(dx: 0.0, dy: 64.0 + 56.0) + snapshotView?.addSubview(contentSnapshotView) + } + return snapshotView + } } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 92ab98ac7c2..261b0b7c5dd 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -988,8 +988,12 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { } var captionIsAboveMedia: Signal = .single(false) + var canMakePaidContent = false + var currentPrice: Int64? if let controller = strongSelf.controller, let mediaPickerContext = controller.mediaPickerContext { captionIsAboveMedia = mediaPickerContext.captionIsAboveMedia + canMakePaidContent = mediaPickerContext.canMakePaidContent + currentPrice = mediaPickerContext.price } let _ = (combineLatest( @@ -1019,7 +1023,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { messageEffect: nil, attachment: true, canSendWhenOnline: sendWhenOnlineAvailable, - forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] + forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], + canMakePaidContent: canMakePaidContent, + currentPrice: currentPrice )), hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, @@ -1041,6 +1047,12 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { schedule: { [weak textInputPanelNode] messageEffect in textInputPanelNode?.sendMessage(.schedule, messageEffect) }, + editPrice: { [weak strongSelf] price in + guard let strongSelf, let controller = strongSelf.controller, let mediaPickerContext = controller.mediaPickerContext else { + return + } + mediaPickerContext.setPrice(price) + }, openPremiumPaywall: { [weak self] c in guard let self else { return diff --git a/submodules/AttachmentUI/Sources/BackButtonNode.swift b/submodules/AttachmentUI/Sources/BackButtonNode.swift new file mode 100644 index 00000000000..5da245a593c --- /dev/null +++ b/submodules/AttachmentUI/Sources/BackButtonNode.swift @@ -0,0 +1,157 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData + +public class WebAppCancelButtonNode: ASDisplayNode { + public enum State { + case cancel + case back + } + + public let buttonNode: HighlightTrackingButtonNode + private let arrowNode: ASImageNode + private let labelNode: ImmediateTextNode + + public var state: State = .cancel + + private var color: UIColor? + + private var _theme: PresentationTheme + public var theme: PresentationTheme { + get { + return self._theme + } + set { + self._theme = newValue + self.setState(self.state, animated: false, animateScale: false, force: true) + } + } + private let strings: PresentationStrings + + private weak var colorSnapshotView: UIView? + + public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) { + let previousColor = self.color + self.color = color + + if case let .animated(duration, curve) = transition, previousColor != color, !self.animatingStateChange { + if let snapshotView = self.view.snapshotContentTree() { + snapshotView.frame = self.bounds + self.view.addSubview(snapshotView) + self.colorSnapshotView = snapshotView + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + self.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + } + } + self.setState(self.state, animated: false, animateScale: false, force: true) + } + + public init(theme: PresentationTheme, strings: PresentationStrings) { + self._theme = theme + self.strings = strings + + self.buttonNode = HighlightTrackingButtonNode() + + self.arrowNode = ASImageNode() + self.arrowNode.displaysAsynchronously = false + + self.labelNode = ImmediateTextNode() + self.labelNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.buttonNode) + self.buttonNode.addSubnode(self.arrowNode) + self.buttonNode.addSubnode(self.labelNode) + + self.buttonNode.highligthedChanged = { [weak self] highlighted in + guard let strongSelf = self else { + return + } + if highlighted { + strongSelf.arrowNode.layer.removeAnimation(forKey: "opacity") + strongSelf.arrowNode.alpha = 0.4 + strongSelf.labelNode.layer.removeAnimation(forKey: "opacity") + strongSelf.labelNode.alpha = 0.4 + } else { + strongSelf.arrowNode.alpha = 1.0 + strongSelf.arrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.labelNode.alpha = 1.0 + strongSelf.labelNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + + self.setState(.cancel, animated: false, force: true) + } + + public func setTheme(_ theme: PresentationTheme, animated: Bool) { + self._theme = theme + var animated = animated + if self.animatingStateChange { + animated = false + } + self.setState(self.state, animated: animated, animateScale: false, force: true) + } + + private var animatingStateChange = false + public func setState(_ state: State, animated: Bool, animateScale: Bool = true, force: Bool = false) { + guard self.state != state || force else { + return + } + self.state = state + + if let colorSnapshotView = self.colorSnapshotView { + self.colorSnapshotView = nil + colorSnapshotView.removeFromSuperview() + } + + if animated, let snapshotView = self.buttonNode.view.snapshotContentTree() { + self.animatingStateChange = true + snapshotView.layer.sublayerTransform = self.buttonNode.subnodeTransform + self.view.addSubview(snapshotView) + + let duration: Double = animateScale ? 0.25 : 0.3 + if animateScale { + snapshotView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.25, removeOnCompletion: false) + } + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + self.animatingStateChange = false + }) + + if animateScale { + self.buttonNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.25) + } + self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + } + + let color = self.color ?? self.theme.rootController.navigationBar.accentTextColor + + self.arrowNode.isHidden = state == .cancel + self.labelNode.attributedText = NSAttributedString(string: state == .cancel ? self.strings.Common_Close : self.strings.Common_Back, font: Font.regular(17.0), textColor: color) + + let labelSize = self.labelNode.updateLayout(CGSize(width: 120.0, height: 56.0)) + + self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: labelSize.width, height: self.buttonNode.frame.height)) + self.arrowNode.image = NavigationBarTheme.generateBackArrowImage(color: color) + if let image = self.arrowNode.image { + self.arrowNode.frame = CGRect(origin: self.arrowNode.frame.origin, size: image.size) + } + self.labelNode.frame = CGRect(origin: self.labelNode.frame.origin, size: labelSize) + self.buttonNode.subnodeTransform = CATransform3DMakeTranslation(state == .back ? 11.0 : 0.0, 0.0, 0.0) + } + + override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: self.buttonNode.frame.width, height: constrainedSize.height)) + self.arrowNode.frame = CGRect(origin: CGPoint(x: -19.0, y: floorToScreenPixels((constrainedSize.height - self.arrowNode.frame.size.height) / 2.0)), size: self.arrowNode.frame.size) + self.labelNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((constrainedSize.height - self.labelNode.frame.size.height) / 2.0)), size: self.labelNode.frame.size) + + return CGSize(width: 70.0, height: 56.0) + } +} diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index e6995380c35..65ceb3a8843 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -242,7 +242,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth let carrier = CTCarrier() let mnc = carrier.mobileNetworkCode ?? "none" - AuthorizationSequenceController.presentEmailComposeController(address: "login@stel.com", subject: strongSelf.presentationData.strings.Login_InvalidPhoneEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_InvalidPhoneEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData) + AuthorizationSequenceController.presentEmailComposeController(address: "recover@telegram.org", subject: strongSelf.presentationData.strings.Login_InvalidPhoneEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_InvalidPhoneEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData) })) case .phoneLimitExceeded: text = strongSelf.presentationData.strings.Login_PhoneFloodError @@ -268,7 +268,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth let carrier = CTCarrier() let mnc = carrier.mobileNetworkCode ?? "none" - AuthorizationSequenceController.presentEmailComposeController(address: "login@stel.com", subject: strongSelf.presentationData.strings.Login_PhoneBannedEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_PhoneBannedEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData) + AuthorizationSequenceController.presentEmailComposeController(address: "recover@telegram.org", subject: strongSelf.presentationData.strings.Login_PhoneBannedEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_PhoneBannedEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData) })) case let .generic(info): text = strongSelf.presentationData.strings.Login_UnknownError @@ -290,7 +290,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth errorString = "unknown" } - AuthorizationSequenceController.presentEmailComposeController(address: "login@stel.com", subject: strongSelf.presentationData.strings.Login_PhoneGenericEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_PhoneGenericEmailBody(formattedNumber, errorString, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData) + AuthorizationSequenceController.presentEmailComposeController(address: "recover@telegram.org", subject: strongSelf.presentationData.strings.Login_PhoneGenericEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_PhoneGenericEmailBody(formattedNumber, errorString, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData) })) case .timeout: text = strongSelf.presentationData.strings.Login_NetworkError diff --git a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift index 634438cafab..15fc596beac 100644 --- a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift +++ b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift @@ -206,7 +206,7 @@ public final class AvatarVideoNode: ASDisplayNode { self.backgroundNode.image = nil let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil)])) + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { self.videoNode?.removeFromSupernode() diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift index bc03da266a3..5ffc394337c 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift @@ -58,7 +58,7 @@ final class BotCheckoutWebInteractionController: ViewController { } override func loadDisplayNode() { - self.displayNode = BotCheckoutWebInteractionControllerNode(presentationData: self.presentationData, url: self.url, intent: self.intent) + self.displayNode = BotCheckoutWebInteractionControllerNode(context: self.context, presentationData: self.presentationData, url: self.url, intent: self.intent) } override func viewDidAppear(_ animated: Bool) { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift index cf6d9647c78..d24ef2092d7 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift @@ -4,6 +4,7 @@ import Display import AsyncDisplayKit import WebKit import TelegramPresentationData +import AccountContext private class WeakPaymentScriptMessageHandler: NSObject, WKScriptMessageHandler { private let f: (WKScriptMessage) -> () @@ -20,12 +21,14 @@ private class WeakPaymentScriptMessageHandler: NSObject, WKScriptMessageHandler } final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode, WKNavigationDelegate { + private let context: AccountContext private var presentationData: PresentationData private let intent: BotCheckoutWebInteractionControllerIntent private var webView: WKWebView? - init(presentationData: PresentationData, url: String, intent: BotCheckoutWebInteractionControllerIntent) { + init(context: AccountContext, presentationData: PresentationData, url: String, intent: BotCheckoutWebInteractionControllerIntent) { + self.context = context self.presentationData = presentationData self.intent = intent @@ -60,6 +63,7 @@ final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode, if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { webView.allowsLinkPreview = false } + webView.navigationDelegate = self case .externalVerification: webView = WKWebView() if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { @@ -142,9 +146,25 @@ final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode, decisionHandler(.cancel) completion(true) } else { + if let url = navigationAction.request.url, let scheme = url.scheme { + let defaultSchemes: [String] = ["http", "https"] + if !defaultSchemes.contains(scheme) { + decisionHandler(.cancel) + self.context.sharedContext.applicationBindings.openUrl(url.absoluteString) + return + } + } decisionHandler(.allow) } } else { + if let url = navigationAction.request.url, let scheme = url.scheme { + let defaultSchemes: [String] = ["http", "https"] + if !defaultSchemes.contains(scheme) { + decisionHandler(.cancel) + self.context.sharedContext.applicationBindings.openUrl(url.absoluteString) + return + } + } decisionHandler(.allow) } } diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index 0fcbb76fd6d..15478db26af 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -10,23 +10,45 @@ swift_library( #"-warnings-as-errors", ], deps = [ - "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/AsyncDisplayKit:AsyncDisplayKit", - "//submodules/Display:Display", - "//submodules/Postbox:Postbox", - "//submodules/TelegramCore:TelegramCore", - "//submodules/TelegramPresentationData:TelegramPresentationData", - "//submodules/TelegramUIPreferences:TelegramUIPreferences", - "//submodules/AppBundle:AppBundle", - "//submodules/InstantPageUI:InstantPageUI", - "//submodules/ContextUI:ContextUI", - "//submodules/UndoUI:UndoUI", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/PresentationDataUtils", + "//submodules/AppBundle", + "//submodules/InstantPageUI", + "//submodules/ContextUI", + "//submodules/UndoUI", + "//submodules/TranslateUI", "//submodules/ComponentFlow:ComponentFlow", "//submodules/Components/ViewControllerComponent:ViewControllerComponent", "//submodules/Components/MultilineTextComponent:MultilineTextComponent", "//submodules/Components/BundleIconComponent:BundleIconComponent", "//submodules/Components/BlurredBackgroundComponent:BlurredBackgroundComponent", "//submodules/TelegramUI/Components/MinimizedContainer", + "//submodules/Pasteboard", + "//submodules/SaveToCameraRoll", + "//submodules/TelegramUI/Components/NavigationStackComponent", + "//submodules/LocationUI", + "//submodules/OpenInExternalAppUI", + "//submodules/GalleryUI", + "//submodules/TelegramUI/Components/ContextReferenceButtonComponent", + "//submodules/Svg", + "//submodules/PromptUI", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/PhotoResources", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/ChatPresentationInterfaceState", + "//submodules/UrlWhitelist", + "//submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode", + "//submodules/SearchUI", + "//submodules/SearchBarNode", + "//submodules/TelegramUI/Components/SaveProgressScreen", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/Utils/DeviceModel", ], visibility = [ "//visibility:public", diff --git a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift new file mode 100644 index 00000000000..da83dddb911 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift @@ -0,0 +1,466 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramPresentationData +import AccountContext +import BundleIconComponent +import MultilineTextComponent +import UrlEscaping + +final class AddressBarContentComponent: Component { + public typealias EnvironmentType = BrowserNavigationBarEnvironment + + let theme: PresentationTheme + let strings: PresentationStrings + let metrics: LayoutMetrics + let url: String + let isSecure: Bool + let isExpanded: Bool + let performAction: ActionSlot + + init( + theme: PresentationTheme, + strings: PresentationStrings, + metrics: LayoutMetrics, + url: String, + isSecure: Bool, + isExpanded: Bool, + performAction: ActionSlot + ) { + self.theme = theme + self.strings = strings + self.metrics = metrics + self.url = url + self.isSecure = isSecure + self.isExpanded = isExpanded + self.performAction = performAction + } + + static func ==(lhs: AddressBarContentComponent, rhs: AddressBarContentComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.metrics != rhs.metrics { + return false + } + if lhs.url != rhs.url { + return false + } + if lhs.isSecure != rhs.isSecure { + return false + } + if lhs.isExpanded != rhs.isExpanded { + return false + } + return true + } + + final class View: UIView, UITextFieldDelegate { + private final class TextField: UITextField { + override func textRect(forBounds bounds: CGRect) -> CGRect { + return bounds.integral + } + + override var canBecomeFirstResponder: Bool { + var canBecomeFirstResponder = super.canBecomeFirstResponder + if !canBecomeFirstResponder && self.alpha.isZero { + canBecomeFirstResponder = true + } + return canBecomeFirstResponder + } + } + + private struct Params: Equatable { + var theme: PresentationTheme + var strings: PresentationStrings + var size: CGSize + var isActive: Bool + var title: String + var isSecure: Bool + var collapseFraction: CGFloat + var isTablet: Bool + + static func ==(lhs: Params, rhs: Params) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.size != rhs.size { + return false + } + if lhs.isActive != rhs.isActive { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.isSecure != rhs.isSecure { + return false + } + if lhs.collapseFraction != rhs.collapseFraction { + return false + } + if lhs.isTablet != rhs.isTablet { + return false + } + return true + } + } + + private let activated: (Bool) -> Void = { _ in } + private let deactivated: (Bool) -> Void = { _ in } + + private let backgroundLayer: SimpleLayer + + private let iconView: UIImageView + + private let clearIconView: UIImageView + private let clearIconButton: HighlightTrackingButton + + private let cancelButtonTitle: ComponentView + private let cancelButton: HighlightTrackingButton + + private var placeholderContent = ComponentView() + private var titleContent = ComponentView() + + private var textFrame: CGRect? + private var textField: TextField? + + private var tapRecognizer: UITapGestureRecognizer? + + private var params: Params? + private var component: AddressBarContentComponent? + + public var wantsDisplayBelowKeyboard: Bool { + return self.textField != nil + } + + init() { + self.backgroundLayer = SimpleLayer() + + self.iconView = UIImageView() + + self.clearIconView = UIImageView() + self.clearIconButton = HighlightableButton() + self.clearIconView.isHidden = false + self.clearIconButton.isHidden = false + + self.cancelButtonTitle = ComponentView() + self.cancelButton = HighlightTrackingButton() + + super.init(frame: CGRect()) + + self.layer.addSublayer(self.backgroundLayer) + + self.addSubview(self.iconView) + self.addSubview(self.clearIconView) + self.addSubview(self.clearIconButton) + + self.addSubview(self.cancelButton) + self.clipsToBounds = true + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + self.tapRecognizer = tapRecognizer + self.addGestureRecognizer(tapRecognizer) + + self.cancelButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view { + cancelButtonTitleView.layer.removeAnimation(forKey: "opacity") + cancelButtonTitleView.alpha = 0.4 + } + } else { + if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view { + cancelButtonTitleView.alpha = 1.0 + cancelButtonTitleView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), for: .touchUpInside) + + self.clearIconButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.clearIconView.layer.removeAnimation(forKey: "opacity") + strongSelf.clearIconView.alpha = 0.4 + } else { + strongSelf.clearIconView.alpha = 1.0 + strongSelf.clearIconView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.clearIconButton.addTarget(self, action: #selector(self.clearPressed), for: .touchUpInside) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state, let component = self.component, !component.isExpanded { + component.performAction.invoke(.openAddressBar) + } + } + + private func activateTextInput() { + self.activated(true) + if let textField = self.textField { + textField.becomeFirstResponder() + Queue.mainQueue().after(0.3, { + textField.selectAll(nil) + }) + } + } + + private func deactivateTextInput() { + self.textField?.endEditing(true) + } + + @objc private func cancelPressed() { + self.deactivated(self.textField?.isFirstResponder ?? false) + + self.component?.performAction.invoke(.closeAddressBar) + } + + @objc private func clearPressed() { + guard let textField = self.textField else { + return + } + textField.text = "" + self.textFieldChanged(textField) + } + + public func textFieldDidBeginEditing(_ textField: UITextField) { + } + + public func textFieldDidEndEditing(_ textField: UITextField) { + } + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + if let component = self.component { + let finalUrl = explicitUrl(textField.text ?? "") + component.performAction.invoke(.navigateTo(finalUrl, true)) + } + textField.endEditing(true) + return false + } + + @objc private func textFieldChanged(_ textField: UITextField) { + let text = textField.text ?? "" + + self.clearIconView.isHidden = text.isEmpty + self.clearIconButton.isHidden = text.isEmpty + self.placeholderContent.view?.isHidden = !text.isEmpty + + if let params = self.params { + self.update(theme: params.theme, strings: params.strings, size: params.size, isActive: params.isActive, title: params.title, isSecure: params.isSecure, collapseFraction: params.collapseFraction, isTablet: params.isTablet, transition: .immediate) + } + } + + func update(component: AddressBarContentComponent, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { + let collapseFraction = environment[BrowserNavigationBarEnvironment.self].fraction + + let wasExpanded = self.component?.isExpanded ?? false + self.component = component + + if !wasExpanded && component.isExpanded { + self.activateTextInput() + } + if wasExpanded && !component.isExpanded { + self.deactivateTextInput() + } + let isActive = self.textField?.isFirstResponder ?? false + + let title = getDisplayUrl(component.url, hostOnly: true) + self.update(theme: component.theme, strings: component.strings, size: availableSize, isActive: isActive, title: title.lowercased(), isSecure: component.isSecure, collapseFraction: collapseFraction, isTablet: component.metrics.isTablet, transition: transition) + + return availableSize + } + + public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, isActive: Bool, title: String, isSecure: Bool, collapseFraction: CGFloat, isTablet: Bool, transition: ComponentTransition) { + let params = Params( + theme: theme, + strings: strings, + size: size, + isActive: isActive, + title: title, + isSecure: isSecure, + collapseFraction: collapseFraction, + isTablet: isTablet + ) + + if self.params == params { + return + } + + let isActiveWithText = self.component?.isExpanded ?? false + + if self.params?.theme !== theme { + self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Lock"), color: .white)?.withRenderingMode(.alwaysTemplate) + self.iconView.tintColor = theme.rootController.navigationSearchBar.inputIconColor + self.clearIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white)?.withRenderingMode(.alwaysTemplate) + self.clearIconView.tintColor = theme.rootController.navigationSearchBar.inputClearButtonColor + } + + self.params = params + + let sideInset: CGFloat = 10.0 + let inputHeight: CGFloat = 36.0 + let topInset: CGFloat = (size.height - inputHeight) / 2.0 + + self.backgroundLayer.backgroundColor = theme.rootController.navigationSearchBar.inputFillColor.cgColor + self.backgroundLayer.cornerRadius = 10.5 + transition.setAlpha(layer: self.backgroundLayer, alpha: max(0.0, min(1.0, 1.0 - collapseFraction * 1.5))) + + let cancelTextSize = self.cancelButtonTitle.update( + transition: .immediate, + component: AnyComponent(Text( + text: strings.Common_Cancel, + font: Font.regular(17.0), + color: theme.rootController.navigationBar.accentTextColor + )), + environment: {}, + containerSize: CGSize(width: size.width - 32.0, height: 100.0) + ) + + let cancelButtonSpacing: CGFloat = 8.0 + + var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: size.width - sideInset * 2.0, height: inputHeight)) + if isActiveWithText && !isTablet { + backgroundFrame.size.width -= cancelTextSize.width + cancelButtonSpacing + } + transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame) + + transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height))) + self.cancelButton.isUserInteractionEnabled = isActiveWithText && !isTablet + + let textX: CGFloat = backgroundFrame.minX + sideInset + let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height)) + + let placeholderSize = self.placeholderContent.update( + transition: transition, + component: AnyComponent( + Text(text: strings.WebBrowser_AddressPlaceholder, font: Font.regular(17.0), color: theme.rootController.navigationSearchBar.inputPlaceholderTextColor) + ), + environment: {}, + containerSize: size + ) + if let placeholderContentView = self.placeholderContent.view { + if placeholderContentView.superview == nil { + placeholderContentView.alpha = 0.0 + placeholderContentView.isHidden = true + self.addSubview(placeholderContentView) + } + let placeholderContentFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.midY - placeholderSize.height / 2.0), size: placeholderSize) + transition.setFrame(view: placeholderContentView, frame: placeholderContentFrame) + transition.setAlpha(view: placeholderContentView, alpha: isActiveWithText ? 1.0 : 0.0) + } + + let titleSize = self.titleContent.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationSearchBar.inputTextColor)), + horizontalAlignment: .center, + truncationType: .end, + maximumNumberOfLines: 1 + ) + ), + environment: {}, + containerSize: CGSize(width: size.width - 36.0, height: size.height) + ) + var titleContentFrame = CGRect(origin: CGPoint(x: isActiveWithText ? textFrame.minX : backgroundFrame.midX - titleSize.width / 2.0, y: backgroundFrame.midY - titleSize.height / 2.0), size: titleSize) + if isSecure && !isActiveWithText { + titleContentFrame.origin.x += 7.0 + } + var titleSizeChanged = false + if let titleContentView = self.titleContent.view { + if titleContentView.superview == nil { + self.addSubview(titleContentView) + } + if titleContentView.frame.width != titleContentFrame.size.width { + titleSizeChanged = true + } + transition.setPosition(view: titleContentView, position: titleContentFrame.center) + titleContentView.bounds = CGRect(origin: .zero, size: titleContentFrame.size) + transition.setAlpha(view: titleContentView, alpha: isActiveWithText ? 0.0 : 1.0) + } + + if let image = self.iconView.image { + let iconFrame = CGRect(origin: CGPoint(x: titleContentFrame.minX - image.size.width - 3.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size) + var iconTransition = transition + if titleSizeChanged { + iconTransition = .immediate + } + iconTransition.setFrame(view: self.iconView, frame: iconFrame) + transition.setAlpha(view: self.iconView, alpha: isActiveWithText || !isSecure ? 0.0 : 1.0) + } + + if let image = self.clearIconView.image { + let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size) + transition.setFrame(view: self.clearIconView, frame: iconFrame) + transition.setFrame(view: self.clearIconButton, frame: iconFrame.insetBy(dx: -8.0, dy: -10.0)) + transition.setAlpha(view: self.clearIconView, alpha: isActiveWithText ? 1.0 : 0.0) + self.clearIconButton.isUserInteractionEnabled = isActiveWithText + } + + if let cancelButtonTitleComponentView = self.cancelButtonTitle.view { + if cancelButtonTitleComponentView.superview == nil { + self.addSubview(cancelButtonTitleComponentView) + cancelButtonTitleComponentView.isUserInteractionEnabled = false + } + transition.setFrame(view: cancelButtonTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize)) + transition.setAlpha(view: cancelButtonTitleComponentView, alpha: isActiveWithText && !isTablet ? 1.0 : 0.0) + } + + let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height)) + + let textField: TextField + if let current = self.textField { + textField = current + } else { + textField = TextField(frame: textFieldFrame) + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.keyboardType = .URL + textField.returnKeyType = .go + self.insertSubview(textField, belowSubview: self.clearIconView) + self.textField = textField + + textField.delegate = self + textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) + } + + let address = getDisplayUrl(self.component?.url ?? "", trim: false) + if textField.text != address { + textField.text = address + self.clearIconView.isHidden = address.isEmpty + self.clearIconButton.isHidden = address.isEmpty + self.placeholderContent.view?.isHidden = !address.isEmpty + } + + textField.textColor = theme.rootController.navigationSearchBar.inputTextColor + transition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + sideInset, y: backgroundFrame.minY - UIScreenPixel), size: CGSize(width: backgroundFrame.width - sideInset - 32.0, height: backgroundFrame.height))) + transition.setAlpha(view: textField, alpha: isActiveWithText ? 1.0 : 0.0) + textField.isUserInteractionEnabled = isActiveWithText + } + } + + 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, environment: environment, transition: transition) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift new file mode 100644 index 00000000000..5675dd5a460 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift @@ -0,0 +1,706 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import AccountContext +import TelegramPresentationData +import ContextUI +import UndoUI +import ListActionItemComponent + +final class BrowserAddressListComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let insets: UIEdgeInsets + let metrics: LayoutMetrics + let addressBarFrame: CGRect + let performAction: ActionSlot + let presentInGlobalOverlay: (ViewController) -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + insets: UIEdgeInsets, + metrics: LayoutMetrics, + addressBarFrame: CGRect, + performAction: ActionSlot, + presentInGlobalOverlay: @escaping (ViewController) -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.insets = insets + self.metrics = metrics + self.addressBarFrame = addressBarFrame + self.performAction = performAction + self.presentInGlobalOverlay = presentInGlobalOverlay + } + + static func ==(lhs: BrowserAddressListComponent, rhs: BrowserAddressListComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.insets != rhs.insets { + return false + } + if lhs.metrics != rhs.metrics { + return false + } + if lhs.addressBarFrame != rhs.addressBarFrame { + return false + } + return true + } + + private struct ItemLayout: Equatable { + struct Section: Equatable { + var id: Int + var insets: UIEdgeInsets + var itemHeight: CGFloat + var itemCount: Int + var hasMore: Bool + + var totalHeight: CGFloat + + init( + id: Int, + insets: UIEdgeInsets, + itemHeight: CGFloat, + itemCount: Int, + hasMore: Bool + ) { + self.id = id + self.insets = insets + self.itemHeight = itemHeight + self.itemCount = itemCount + self.hasMore = hasMore + + var totalHeight = insets.top + itemHeight * CGFloat(itemCount) + insets.bottom + if hasMore { + totalHeight -= itemHeight + totalHeight += 44.0 + } + self.totalHeight = totalHeight + } + } + + var containerSize: CGSize + var insets: UIEdgeInsets + var sections: [Section] + + var contentHeight: CGFloat + + init( + containerSize: CGSize, + insets: UIEdgeInsets, + sections: [Section] + ) { + self.containerSize = containerSize + self.insets = insets + self.sections = sections + + var contentHeight: CGFloat = 0.0 + for section in sections { + contentHeight += section.totalHeight + } + self.contentHeight = contentHeight + } + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + final class View: UIView, UIScrollViewDelegate { + struct State { + let recent: [TelegramMediaWebpage] + let isRecentExpanded: Bool + let bookmarks: [Message] + } + + private let outerView = UIButton() + private let shadowView = UIImageView() + private let backgroundView = UIView() + private let scrollView = ScrollView() + private let itemContainerView = UIView() + + private let addressTemplateItem = ComponentView() + + private var visibleSectionHeaders: [Int: ComponentView] = [:] + private var visibleItems: [AnyHashable: ComponentView] = [:] + + private var ignoreScrolling: Bool = false + + private var component: BrowserAddressListComponent? + private weak var state: EmptyComponentState? + private var itemLayout: ItemLayout? + + private var stateDisposable: Disposable? + private var stateValue: State? + private let isRecentExpanded = ValuePromise(false) + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundView.clipsToBounds = true + + self.scrollView.alwaysBounceVertical = true + self.scrollView.delegate = self + self.scrollView.showsVerticalScrollIndicator = false + + self.addSubview(self.outerView) + self.addSubview(self.shadowView) + self.addSubview(self.backgroundView) + self.backgroundView.addSubview(self.scrollView) + self.scrollView.addSubview(self.itemContainerView) + + self.outerView.addTarget(self, action: #selector(self.outerPressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError() + } + + deinit { + self.stateDisposable?.dispose() + } + + @objc private func outerPressed() { + self.component?.performAction.invoke(.closeAddressBar) + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.window?.endEditing(true) + + cancelContextGestures(view: scrollView) + } + + private func updateScrolling(transition: ComponentTransition) { + guard let component = self.component, let itemLayout = self.itemLayout, let state = self.stateValue else { + return + } + + var topOffset = -self.scrollView.bounds.minY + topOffset = max(0.0, topOffset) + + let visibleBounds = self.scrollView.bounds + var visibleFrame = self.scrollView.frame + visibleFrame.origin.x = 0.0 + + var validIds: [AnyHashable] = [] + var validSectionHeaders: [AnyHashable] = [] + var sectionOffset: CGFloat = 0.0 + + let sideInset: CGFloat = 0.0 + let containerInset: CGFloat = 0.0 + + for sectionIndex in 0 ..< itemLayout.sections.count { + let section = itemLayout.sections[sectionIndex] + + do { + var sectionHeaderFrame = CGRect(origin: CGPoint(x: sideInset, y: sectionOffset - self.scrollView.bounds.minY), size: CGSize(width: itemLayout.containerSize.width, height: section.insets.top)) + + let sectionHeaderMinY = topOffset + containerInset + let sectionHeaderMaxY = containerInset + sectionOffset - self.scrollView.bounds.minY + section.totalHeight - 28.0 + + sectionHeaderFrame.origin.y = max(sectionHeaderFrame.origin.y, sectionHeaderMinY) + sectionHeaderFrame.origin.y = min(sectionHeaderFrame.origin.y, sectionHeaderMaxY) + + if visibleFrame.intersects(sectionHeaderFrame) { + validSectionHeaders.append(section.id) + let sectionHeader: ComponentView + var sectionHeaderTransition = transition + if let current = self.visibleSectionHeaders[section.id] { + sectionHeader = current + } else { + if !transition.animation.isImmediate { + sectionHeaderTransition = .immediate + } + sectionHeader = ComponentView() + self.visibleSectionHeaders[section.id] = sectionHeader + } + + let sectionTitle: String + if section.id == 0 { + sectionTitle = component.strings.WebBrowser_AddressBar_RecentlyVisited + } else if section.id == 1 { + sectionTitle = component.strings.WebBrowser_AddressBar_Bookmarks + } else { + sectionTitle = "" + } + + let _ = sectionHeader.update( + transition: sectionHeaderTransition, + component: AnyComponent(SectionHeaderComponent( + theme: component.theme, + style: .plain, + title: sectionTitle, + insets: component.insets, + actionTitle: section.id == 0 ? component.strings.WebBrowser_AddressBar_RecentlyVisited_Clear : nil, + action: { [weak self] in + if let self, let component = self.component { + let _ = clearRecentlyVisitedLinks(engine: component.context.engine).start() + } + } + )), + environment: {}, + containerSize: sectionHeaderFrame.size + ) + if let sectionHeaderView = sectionHeader.view { + if sectionHeaderView.superview == nil { + self.backgroundView.addSubview(sectionHeaderView) + + if !transition.animation.isImmediate { + sectionHeaderView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + let sectionXOffset = self.scrollView.frame.minX + sectionHeaderTransition.setFrame(view: sectionHeaderView, frame: sectionHeaderFrame.offsetBy(dx: sectionXOffset, dy: 0.0)) + } + } + } + + for i in 0 ..< section.itemCount { + var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) + if !visibleBounds.intersects(itemFrame) { + continue + } + + var isMore = false + if section.hasMore && i == 3 { + isMore = true + itemFrame.size.height = 44.0 + } + + var id: String = "" + if section.id == 0 { + id = "recent_\(state.recent[i].content.url ?? "")" + if isMore { + id = "recent_more" + } + } else if section.id == 1 { + id = "bookmark_\(state.bookmarks[i].id.id)" + if isMore { + id = "bookmark_more" + } + } + + let itemId = AnyHashable(id) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.visibleItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.visibleItems[itemId] = visibleItem + } + + if isMore { + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + ListActionItemComponent( + theme: component.theme, + title: AnyComponent(Text( + text: component.strings.WebBrowser_AddressBar_ShowMore, + font: Font.regular(17.0), + color: component.theme.list.itemAccentColor + )), + leftIcon: .custom( + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(Image( + image: PresentationResourcesItemList.downArrowImage(component.theme), + size: CGSize(width: 30.0, height: 30.0) + )) + ), + false + ), + accessory: nil, + action: { [weak self] _ in + self?.isRecentExpanded.set(true) + }, + highlighting: .default, + updateIsHighlighted: { view, _ in + + }) + ), + environment: {}, + containerSize: itemFrame.size + ) + } else { + var webPage: TelegramMediaWebpage? + var itemMessage: Message? + + if section.id == 0 { + webPage = state.recent[i] + } else if section.id == 1 { + let message = state.bookmarks[i] + if let primaryUrl = getPrimaryUrl(message: message) { + if let media = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage { + webPage = media + } else { + webPage = TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: primaryUrl, displayUrl: "", hash: 0, type: nil, websiteName: "", title: message.text, text: "", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil))) + } + itemMessage = message + } else { + continue + } + } + + let performAction = component.performAction + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + BrowserAddressListItemComponent( + context: component.context, + theme: component.theme, + webPage: webPage!, + message: itemMessage, + hasNext: true, + insets: component.insets, + action: { + if let url = webPage?.content.url { + performAction.invoke(.navigateTo(url, false)) + } + }, + contextAction: { [weak self] webPage, message, sourceView, gesture in + guard let self, let component = self.component, let url = webPage.content.url else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + var itemList: [ContextMenuItem] = [] + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_CopyLink, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + UIPasteboard.general.string = url + if let self, let component = self.component { + component.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) + } + }))) + + if let message { + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_DeleteBookmark, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + if let self, let component = self.component { + let _ = component.context.engine.messages.deleteMessagesInteractively(messageIds: [message.id], type: .forEveryone).startStandalone() + } + }))) + } else { + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_RemoveRecent, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + if let self, let component = self.component, let url = webPage.content.url { + let _ = removeRecentlyVisitedLink(engine: component.context.engine, url: url).startStandalone() + } + }))) + } + + let items = ContextController.Items(content: .list(itemList)) + let controller = ContextController( + presentationData: presentationData, + source: .extracted(BrowserAddressListContextExtractedContentSource(contentView: sourceView)), + items: .single(items), + recognizer: nil, + gesture: gesture + ) + component.presentInGlobalOverlay(controller) + }) + ), + environment: {}, + containerSize: itemFrame.size + ) + } + if let itemView = visibleItem.view { + if itemView.superview == nil { + self.itemContainerView.addSubview(itemView) + if !transition.animation.isImmediate { + transition.animateAlpha(view: itemView, from: 0.0, to: 1.0) + } + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + } + + sectionOffset += section.totalHeight + } + + var removeIds: [AnyHashable] = [] + for (id, item) in self.visibleItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } + } + } + for id in removeIds { + self.visibleItems.removeValue(forKey: id) + } + + var removeSectionHeaderIds: [Int] = [] + for (id, item) in self.visibleSectionHeaders { + if !validSectionHeaders.contains(id) { + removeSectionHeaderIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } + } + } + for id in removeSectionHeaderIds { + self.visibleSectionHeaders.removeValue(forKey: id) + } + } + + func update(component: BrowserAddressListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + if self.component == nil { + self.stateDisposable = combineLatest(queue: Queue.mainQueue(), + recentlyVisitedLinks(engine: component.context.engine), + self.isRecentExpanded.get(), + component.context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: component.context.account.peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 100, fixedCombinedReadStates: nil, tag: .tag(.webPage)) + ).start(next: { [weak self] recent, isRecentExpanded, view in + guard let self else { + return + } + + var bookmarks: [Message] = [] + for entry in view.0.entries.reversed() { + bookmarks.append(entry.message) + } + + let isFirstTime = self.stateValue == nil + self.stateValue = State( + recent: recent, + isRecentExpanded: isRecentExpanded, + bookmarks: bookmarks + ) + self.state?.updated(transition: isFirstTime ? .immediate : .easeInOut(duration: 0.25)) + }) + } + + self.component = component + self.state = state + + self.outerView.isHidden = !component.metrics.isTablet + self.outerView.frame = CGRect(origin: .zero, size: availableSize) + + let containerFrame: CGRect + if component.metrics.isTablet { + let containerSize = CGSize(width: component.addressBarFrame.width + 32.0, height: 540.0) + containerFrame = CGRect(origin: CGPoint(x: floor(component.addressBarFrame.center.x - containerSize.width / 2.0), y: 72.0), size: containerSize) + + self.backgroundView.layer.cornerRadius = 10.0 + } else { + containerFrame = CGRect(origin: .zero, size: availableSize) + + self.backgroundView.layer.cornerRadius = 0.0 + } + + let resetScrolling = self.scrollView.bounds.width != containerFrame.width + if themeUpdated { + self.backgroundView.backgroundColor = component.theme.list.plainBackgroundColor + } + + let itemsContainerWidth = availableSize.width + let addressItemSize = self.addressTemplateItem.update( + transition: .immediate, + component: AnyComponent(BrowserAddressListItemComponent( + context: component.context, + theme: component.theme, + webPage: TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil))), + message: nil, + hasNext: true, + insets: .zero, + action: {}, + contextAction: nil + )), + environment: {}, + containerSize: CGSize(width: itemsContainerWidth, height: 1000.0) + ) + + var sections: [ItemLayout.Section] = [] + if let state = self.stateValue { + if !state.recent.isEmpty { + var recentCount = state.recent.count + var hasMore = false + if recentCount > 4 && !state.isRecentExpanded { + recentCount = 4 + hasMore = true + } + sections.append(ItemLayout.Section( + id: 0, + insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), + itemHeight: addressItemSize.height, + itemCount: recentCount, + hasMore: hasMore + )) + } + if !state.bookmarks.isEmpty { + sections.append(ItemLayout.Section( + id: 1, + insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), + itemHeight: addressItemSize.height, + itemCount: state.bookmarks.count, + hasMore: false + )) + } + } + + let itemLayout = ItemLayout(containerSize: containerFrame.size, insets: .zero, sections: sections) + self.itemLayout = itemLayout + + let containerWidth = containerFrame.size.width + let scrollContentHeight = max(itemLayout.contentHeight, containerFrame.size.height) + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: .zero, size: containerFrame.size)) + let contentSize = CGSize(width: containerWidth, height: scrollContentHeight) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } + if resetScrolling { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: containerFrame.size.height)) + } + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + transition.setFrame(view: self.backgroundView, frame: containerFrame) + transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: .zero, size: CGSize(width: containerWidth, height: scrollContentHeight))) + + if component.metrics.isTablet { + transition.setFrame(view: self.shadowView, frame: containerFrame.insetBy(dx: -60.0, dy: -60.0)) + self.shadowView.isHidden = false + if self.shadowView.image == nil { + self.shadowView.image = generateShadowImage() + } + } else { + self.shadowView.isHidden = true + } + + return availableSize + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if let component = self.component, component.metrics.isTablet { + let addressFrame = CGRect(origin: CGPoint(x: self.backgroundView.frame.minX, y: self.backgroundView.frame.minY - 48.0), size: CGSize(width: self.backgroundView.frame.width, height: 48.0)) + if addressFrame.contains(point) { + return nil + } + } + return result + } + } + + 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 func generateShadowImage() -> UIImage? { + return generateImage(CGSize(width: 140.0, height: 140.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.saveGState() + context.setShadow(offset: CGSize(), blur: 60.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor) + + let path = UIBezierPath(roundedRect: CGRect(x: 60.0, y: 60.0, width: 20.0, height: 20.0), cornerRadius: 10.0).cgPath + context.addPath(path) + context.fillPath() + + context.restoreGState() + + context.setBlendMode(.clear) + context.addPath(path) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 70, topCapHeight: 70) +} + +private final class BrowserAddressListContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = false + let blurBackground: Bool = true + + private let contentView: ContextExtractedContentContainingView + + init(contentView: ContextExtractedContentContainingView) { + self.contentView = contentView + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} + +private func cancelContextGestures(view: UIView) { + if let gestureRecognizers = view.gestureRecognizers { + for gesture in gestureRecognizers { + if let gesture = gesture as? ContextGesture { + gesture.cancel() + } + } + } + for subview in view.subviews { + cancelContextGestures(view: subview) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift new file mode 100644 index 00000000000..f41a5395ea9 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift @@ -0,0 +1,364 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import MultilineTextComponent +import TelegramPresentationData +import PhotoResources +import AccountContext +import ContextUI +import UrlEscaping + +private let iconFont = Font.with(size: 30.0, design: .round, weight: .bold) +private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 6.0, color: UIColor(rgb: 0xFF9500)) + +final class BrowserAddressListItemComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let webPage: TelegramMediaWebpage + var message: Message? + let hasNext: Bool + let insets: UIEdgeInsets + let action: () -> Void + let contextAction: ((TelegramMediaWebpage, Message?, ContextExtractedContentContainingView, ContextGesture) -> Void)? + + init( + context: AccountContext, + theme: PresentationTheme, + webPage: TelegramMediaWebpage, + message: Message?, + hasNext: Bool, + insets: UIEdgeInsets, + action: @escaping () -> Void, + contextAction: ((TelegramMediaWebpage, Message?, ContextExtractedContentContainingView, ContextGesture) -> Void)? + ) { + self.context = context + self.theme = theme + self.webPage = webPage + self.message = message + self.hasNext = hasNext + self.insets = insets + self.action = action + self.contextAction = contextAction + } + + static func ==(lhs: BrowserAddressListItemComponent, rhs: BrowserAddressListItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.webPage != rhs.webPage { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + if lhs.insets != rhs.insets { + return false + } + return true + } + + final class View: ContextControllerSourceView { + private let extractedContainerView = ContextExtractedContentContainingView() + private let containerButton = HighlightTrackingButton() + + private let separatorLayer = SimpleLayer() + private var highlightedBackgroundLayer = SimpleLayer() + private var emptyIcon: UIImageView? + private var emptyLabel: ComponentView? + private var icon = TransformImageNode() + private let title = ComponentView() + private let subtitle = ComponentView() + + private var isExtractedToContextMenu: Bool = false + + private var component: BrowserAddressListItemComponent? + private weak var state: EmptyComponentState? + + private var currentIconImageRepresentation: TelegramMediaImageRepresentation? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + self.layer.addSublayer(self.highlightedBackgroundLayer) + + self.addSubview(self.extractedContainerView) + self.targetViewForActivationProgress = self.extractedContainerView.contentView + + self.highlightedBackgroundLayer.opacity = 0.0 + + self.extractedContainerView.contentView.addSubview(self.containerButton) + + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.containerButton.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + self.superview?.bringSubviewToFront(self) + self.highlightedBackgroundLayer.removeAnimation(forKey: "opacity") + self.highlightedBackgroundLayer.opacity = 1.0 + } else { + self.highlightedBackgroundLayer.opacity = 0.0 + self.highlightedBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + + self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in + guard let self, let component = self.component else { + return + } + self.containerButton.clipsToBounds = value + self.containerButton.backgroundColor = value ? component.theme.list.plainBackgroundColor : nil + self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0 + + if value { + self.highlightedBackgroundLayer.opacity = 0.0 + } + } + self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in + guard let self else { + return + } + self.isExtractedToContextMenu = value + + let mappedTransition: ComponentTransition + if value { + mappedTransition = ComponentTransition(transition) + } else { + mappedTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) + } + self.state?.updated(transition: mappedTransition) + } + + self.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + gesture.cancel() + return + } + component.contextAction?(component.webPage, component.message, self.extractedContainerView, gesture) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action() + } + + func update(component: BrowserAddressListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + let currentIconImageRepresentation = self.currentIconImageRepresentation + + let iconSize = CGSize(width: 40.0, height: 40.0) + let height: CGFloat = 60.0 + let leftInset: CGFloat = component.insets.left + 11.0 + iconSize.width + 11.0 + let rightInset: CGFloat = 16.0 + let titleSpacing: CGFloat = 2.0 + let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0 + + let title: String + let subtitle: String + var parsedUrl: URL? + var iconImageReferenceAndRepresentation: (AnyMediaReference, TelegramMediaImageRepresentation)? + var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + + if case let .Loaded(content) = component.webPage.content { + title = content.title ?? content.url + + subtitle = getDisplayUrl(content.url) + + parsedUrl = URL(string: content.url) + + if let image = content.image { + if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 80, height: 80)) { + if let message = component.message { + iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: image), representation) + } else { + iconImageReferenceAndRepresentation = (.standalone(media: image), representation) + } + } + } else if let file = content.file { + if let representation = smallestImageRepresentation(file.previewRepresentations) { + if let message = component.message { + iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: file), representation) + } else { + iconImageReferenceAndRepresentation = (.standalone(media: file), representation) + } + } + } + + if currentIconImageRepresentation != iconImageReferenceAndRepresentation?.1 { + if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation { + if let imageReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaImage.self) { + updateIconImageSignal = chatWebpageSnippetPhoto(account: component.context.account, userLocation: (component.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, photoReference: imageReference) + } else if let fileReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaFile.self) { + updateIconImageSignal = chatWebpageSnippetFile(account: component.context.account, userLocation: (component.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, mediaReference: fileReference.abstract, representation: iconImageReferenceAndRepresentation.1) + } + } else { + updateIconImageSignal = .complete() + } + } + } else { + title = "" + subtitle = "" + } + + self.component = component + self.state = state + self.currentIconImageRepresentation = iconImageReferenceAndRepresentation?.1 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: component.theme.list.itemAccentColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let centralContentHeight = titleSize.height + subtitleSize.height + titleSpacing + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + } + + let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + subtitleView.isUserInteractionEnabled = false + self.containerButton.addSubview(subtitleView) + } + subtitleView.frame = subtitleFrame + } + + + let iconFrame = CGRect(origin: CGPoint(x: 11.0 + component.insets.left, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize) + + let iconImageLayout = self.icon.asyncLayout() + var iconImageApply: (() -> Void)? + if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation { + let imageCorners = ImageCorners(radius: 6.0) + let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageReferenceAndRepresentation.1.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor) + iconImageApply = iconImageLayout(arguments) + } + + if let iconImageApply = iconImageApply { + if let updateImageSignal = updateIconImageSignal { + self.icon.setSignal(updateImageSignal) + } + + if self.icon.supernode == nil { + self.containerButton.addSubview(self.icon.view) + self.icon.frame = iconFrame + } else { + transition.setFrame(view: self.icon.view, frame: iconFrame) + } + + iconImageApply() + + if let emptyIcon = self.emptyIcon { + self.emptyIcon = nil + emptyIcon.removeFromSuperview() + } + if let emptyLabel = self.emptyLabel { + self.emptyLabel = nil + emptyLabel.view?.removeFromSuperview() + } + } else { + if self.icon.supernode != nil { + self.icon.view.removeFromSuperview() + } + + let icon: UIImageView + let label: ComponentView + if let currentEmptyIcon = self.emptyIcon, let currentEmptyLabel = self.emptyLabel { + icon = currentEmptyIcon + label = currentEmptyLabel + } else { + icon = UIImageView() + icon.image = iconTextBackgroundImage + self.containerButton.addSubview(icon) + + label = ComponentView() + } + icon.frame = iconFrame + + var iconText = "" + if let parsedUrl, let host = parsedUrl.host { + if parsedUrl.path.hasPrefix("/addstickers/") { + iconText = "S" + } else if parsedUrl.path.hasPrefix("/addemoji/") { + iconText = "E" + } else { + iconText = host[.. 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) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift new file mode 100644 index 00000000000..b77e831e36f --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -0,0 +1,582 @@ +import Foundation +import UIKit +import AccountContext +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import PresentationDataUtils +import ChatControllerInteraction +import TelegramUIPreferences +import ChatPresentationInterfaceState +import TextFormat +import UrlWhitelist +import SearchUI +import SearchBarNode +import ChatHistorySearchContainerNode +import ContextUI +import UndoUI + +public final class BrowserBookmarksScreen: ViewController { + final class Node: ViewControllerTracingNode, ASScrollViewDelegate { + private let context: AccountContext + private var presentationData: PresentationData + private weak var controller: BrowserBookmarksScreen? + + private let controllerInteraction: ChatControllerInteraction + private var searchDisplayController: SearchDisplayController? + + fileprivate let historyNode: ChatHistoryListNode + private let bottomPanelNode: BottomPanelNode + + private var addedBookmark = false + + private var validLayout: (ContainerViewLayout, CGFloat, CGFloat)? + + init(context: AccountContext, controller: BrowserBookmarksScreen, presentationData: PresentationData) { + self.context = context + self.controller = controller + self.presentationData = presentationData + + var openMessageImpl: ((Message) -> Bool)? + var openContextMenuImpl: ((Message, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void)? + self.controllerInteraction = ChatControllerInteraction(openMessage: { message, _ in + if let openMessageImpl = openMessageImpl { + return openMessageImpl(message) + } else { + return false + } + }, openPeer: { _, _, _, _ in + }, openPeerMention: { _, _ in + }, openMessageContextMenu: { message, _, sourceView, rect, gesture, _ in + openContextMenuImpl?(message, sourceView, rect, gesture) + }, openMessageReactionContextMenu: { _, _, _, _ in + }, updateMessageReaction: { _, _, _, _ in + }, activateMessagePinch: { _ in + }, openMessageContextActions: { _, _, _, _ in + }, navigateToMessage: { _, _, _ in + }, navigateToMessageStandalone: { _ in + }, navigateToThreadMessage: { _, _, _ in + }, tapMessage: nil, clickThroughMessage: { _, _ in + }, toggleMessagesSelection: { _, _ in + }, sendCurrentMessage: { _, _ in + }, sendMessage: { _ in + }, sendSticker: { _, _, _, _, _, _, _, _, _ in + return false + }, sendEmoji: { _, _, _ in + }, sendGif: { _, _, _, _, _ in + return false + }, sendBotContextResultAsGif: { _, _, _, _, _, _ in + return false + }, requestMessageActionCallback: { _, _, _, _ in + }, requestMessageActionUrlAuth: { _, _ in + }, activateSwitchInline: { _, _, _ in + }, openUrl: { [weak controller] url in + if let controller { + controller.openUrl(url.url) + controller.dismiss() + } + }, shareCurrentLocation: { + }, shareAccountContact: { + }, sendBotCommand: { _, _ in + }, openInstantPage: { message, _ in + if let openMessageImpl = openMessageImpl { + let _ = openMessageImpl(message) + } + }, openWallpaper: { _ in + }, openTheme: {_ in + }, openHashtag: { _, _ in + }, updateInputState: { _ in + }, updateInputMode: { _ in + }, openMessageShareMenu: { _ in + }, presentController: { _, _ in + }, presentControllerInCurrent: { _, _ in + }, navigationController: { + return nil + }, chatControllerNode: { + return nil + }, presentGlobalOverlayController: { _, _ in + }, callPeer: { _, _ in + }, longTap: { _, _ in + }, openCheckoutOrReceipt: { _, _ in + }, openSearch: { + }, setupReply: { _ in + }, canSetupReply: { _ in + return .none + }, canSendMessages: { + return false + }, navigateToFirstDateMessage: { _, _ in + }, requestRedeliveryOfFailedMessages: { _ in + }, addContact: { _ in + }, rateCall: { _, _, _ in + }, requestSelectMessagePollOptions: { _, _ in + }, requestOpenMessagePollResults: { _, _ in + }, openAppStorePage: { + }, displayMessageTooltip: { _, _, _, _, _ in + }, seekToTimecode: { _, _, _ in + }, scheduleCurrentMessage: { _ in + }, sendScheduledMessagesNow: { _ in + }, editScheduledMessagesTime: { _ in + }, performTextSelectionAction: { _, _, _, _ in + }, displayImportedMessageTooltip: { _ in + }, displaySwipeToReplyHint: { + }, dismissReplyMarkupMessage: { _ in + }, openMessagePollResults: { _, _ in + }, openPollCreation: { _ in + }, displayPollSolution: { _, _ in + }, displayPsa: { _, _ in + }, displayDiceTooltip: { _ in + }, animateDiceSuccess: { _, _ in + }, displayPremiumStickerTooltip: { _, _ in + }, displayEmojiPackTooltip: { _, _ in + }, openPeerContextMenu: { _, _, _, _, _ in + }, openMessageReplies: { _, _, _ in + }, openReplyThreadOriginalMessage: { _ in + }, openMessageStats: { _ in + }, editMessageMedia: { _, _ in + }, copyText: { _ in + }, displayUndo: { _ in + }, isAnimatingMessage: { _ in + return false + }, getMessageTransitionNode: { + return nil + }, updateChoosingSticker: { _ in + }, commitEmojiInteraction: { _, _, _, _ in + }, openLargeEmojiInfo: { _, _, _ in + }, openJoinLink: { _ in + }, openWebView: { _, _, _, _ in + }, activateAdAction: { _, _ in + }, openRequestedPeerSelection: { _, _, _, _ in + }, saveMediaToFiles: { _ in + }, openNoAdsDemo: { + }, openAdsInfo: { + }, displayGiveawayParticipationStatus: { _ in + }, openPremiumStatusInfo: { _, _, _, _ in + }, openRecommendedChannelContextMenu: { _, _, _ in + }, openGroupBoostInfo: { _, _ in + }, openStickerEditor: { + }, openAgeRestrictedMessageMedia: { _, _ in + }, playMessageEffect: { _ in + }, editMessageFactCheck: { _ in + }, requestMessageUpdate: { _, _ in + }, cancelInteractiveKeyboardGestures: { + }, dismissTextInput: { + }, scrollToMessageId: { _ in + }, navigateToStory: { _, _ in + }, attemptedNavigationToPrivateQuote: { _ in + }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) + + + let tagMask: MessageTags = .webPage + let chatLocationContextHolder = Atomic(value: nil) + self.historyNode = context.sharedContext.makeChatHistoryListNode( + context: context, + updatedPresentationData: (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), + chatLocation: .peer(id: context.account.peerId), + chatLocationContextHolder: chatLocationContextHolder, + tag: .tag(tagMask), + source: .default, + subject: nil, + controllerInteraction: self.controllerInteraction, + selectedMessages: .single(nil), + mode: .list( + search: false, + reversed: false, + reverseGroups: false, + displayHeaders: .none, + hintLinks: true, + isGlobalSearch: false + ) + ) + + var addBookmarkImpl: (() -> Void)? + self.bottomPanelNode = BottomPanelNode(theme: presentationData.theme, strings: presentationData.strings, action: { + addBookmarkImpl?() + }) + + super.init() + + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + self.addSubnode(self.historyNode) + self.addSubnode(self.bottomPanelNode) + + openMessageImpl = { [weak controller] message in + guard let controller else { + return false + } + if let primaryUrl = getPrimaryUrl(message: message) { + controller.openUrl(primaryUrl) + } + controller.dismiss() + return true + } + + addBookmarkImpl = { [weak self] in + guard let self else { + return + } + self.controller?.addBookmark() + self.addedBookmark = true + if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) + } + } + + openContextMenuImpl = { [weak self] message, sourceNode, rect, gesture in + guard let self, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else { + return + } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + var itemList: [ContextMenuItem] = [] + if let webPage = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, let url = webPage.content.url { + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_CopyLink, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + UIPasteboard.general.string = url + if let self { + self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + }))) + } + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_DeleteBookmark, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + if let self { + let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: [message.id], type: .forEveryone).startStandalone() + } + }))) + + let items = ContextController.Items(content: .list(itemList)) + let controller = ContextController( + presentationData: presentationData, + source: .extracted(BrowserBookmarksContextExtractedContentSource(contentNode: sourceNode)), + items: .single(items), + recognizer: nil, + gesture: gesture as? ContextGesture + ) + self.controller?.presentInGlobalOverlay(controller) + } + } + + func activateSearch(placeholderNode: SearchBarPlaceholderNode) { + guard let (layout, navigationBarHeight, _) = self.validLayout, let navigationBar = self.controller?.navigationBar else { + return + } + let tagMask: MessageTags = .webPage + + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, placeholder: self.presentationData.strings.Common_Search, hasBackground: true, contentNode: ChatHistorySearchContainerNode(context: self.context, peerId: self.context.account.peerId, threadId: nil, tagMask: tagMask, interfaceInteraction: self.controllerInteraction), cancel: { [weak self] in + self?.controller?.deactivateSearch() + }) + + self.searchDisplayController?.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in + if let strongSelf = self, let placeholderNode { + if isSearchBar { + placeholderNode.supernode?.insertSubnode(subnode, aboveSubnode: placeholderNode) + } else { + strongSelf.insertSubnode(subnode, belowSubnode: navigationBar) + } + } + }, placeholder: placeholderNode) + } + + func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) { + guard let searchDisplayController = self.searchDisplayController else { + return + } + self.searchDisplayController = nil + searchDisplayController.deactivate(placeholder: placeholderNode) + } + + func scrollToTop() { + self.historyNode.scrollToEndOfHistory() + } + + func containerLayoutUpdated(layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (layout, navigationBarHeight, actualNavigationBarHeight) + + let historyFrame = CGRect(origin: .zero, size: layout.size) + transition.updateFrame(node: self.historyNode, frame: historyFrame) + + var insets = layout.insets(options: [.input]) + insets.top += navigationBarHeight + + var headerInsets = layout.insets(options: [.input]) + headerInsets.top += actualNavigationBarHeight + + let panelHeight = self.bottomPanelNode.updateLayout(width: layout.size.width, sideInset: layout.safeInsets.left, bottomInset: insets.bottom, transition: transition) + var panelOrigin: CGFloat = layout.size.height + if !self.addedBookmark { + panelOrigin -= panelHeight + insets.bottom = panelHeight + } + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelOrigin), size: CGSize(width: layout.size.width, height: panelHeight)) + transition.updateFrame(node: self.bottomPanelNode, frame: panelFrame) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: historyFrame.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve) + self.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) + + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + } + } + + private let context: AccountContext + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + private let url: String + private let openUrl: (String) -> Void + private let addBookmark: () -> Void + + private var controllerNode: Node { + return self.displayNode as! Node + } + + private var searchContentNode: NavigationBarSearchContentNode? + + private var validLayout: ContainerViewLayout? + + private var node: Node { + return self.displayNode as! Node + } + + public init(context: AccountContext, url: String, openUrl: @escaping (String) -> Void, addBookmark: @escaping () -> Void) { + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.url = url + self.openUrl = openUrl + self.addBookmark = addBookmark + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + + self.navigationPresentation = .modal + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.title = self.presentationData.strings.WebBrowser_Bookmarks_Title + + self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, activate: { [weak self] in + self?.activateSearch() + }) + self.navigationBar?.setContentNode(self.searchContentNode, animated: false) + + self.scrollToTop = { [weak self] in + if let self { + if let searchContentNode = self.searchContentNode { + searchContentNode.updateExpansionProgress(1.0, animated: true) + } + self.node.scrollToTop() + } + } + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings() + } + } + }).strict() + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = Node(context: self.context, controller: self, presentationData: self.presentationData) + + self.node.historyNode.contentPositionChanged = { [weak self] offset in + if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode { + searchContentNode.updateListVisibleContentOffset(offset) + } + } +// +// self.node.historyNode.didEndScrolling = { [weak self] _ in +// if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode { +// let _ = fixNavigationSearchableListNodeScrolling(strongSelf.node.historyNode, searchNode: searchContentNode) +// } +// } + + self.displayNodeDidLoad() + } + + private func updateThemeAndStrings() { + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) + self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search) + } + + fileprivate func activateSearch() { + if self.displayNavigationBar { + if let scrollToTop = self.scrollToTop { + scrollToTop() + } + if let searchContentNode = self.searchContentNode { + self.node.activateSearch(placeholderNode: searchContentNode.placeholderNode) + } + self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring)) + } + } + + fileprivate func deactivateSearch() { + if !self.displayNavigationBar { + self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) + if let searchContentNode = self.searchContentNode { + self.node.deactivateSearch(placeholderNode: searchContentNode.placeholderNode) + } + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.validLayout = layout + + self.controllerNode.containerLayoutUpdated(layout: layout, navigationBarHeight: self.cleanNavigationHeight, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + } + + @objc private func cancelPressed() { + self.dismiss() + } +} + +private class BottomPanelNode: ASDisplayNode { + private let theme: PresentationTheme + private let strings: PresentationStrings + private let action: () -> Void + + private let separatorNode: ASDisplayNode + private let button: HighlightTrackingButtonNode + private let iconNode: ASImageNode + private let textNode: ImmediateTextNode + + private var validLayout: (CGFloat, CGFloat, CGFloat)? + + init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) { + self.theme = theme + self.strings = strings + self.action = action + + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/AddIcon"), color: theme.rootController.navigationBar.accentTextColor) + self.iconNode.isUserInteractionEnabled = false + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.attributedText = NSAttributedString(string: strings.WebBrowser_Bookmarks_BookmarkCurrent, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) + self.textNode.isUserInteractionEnabled = false + + self.button = HighlightTrackingButtonNode() + + super.init() + + self.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor + + self.addSubnode(self.button) + self.addSubnode(self.separatorNode) + self.addSubnode(self.iconNode) + self.addSubnode(self.textNode) + self.addSubnode(self.button) + + self.button.highligthedChanged = { [weak self] highlighted in + if let self { + if highlighted { + self.iconNode.layer.removeAnimation(forKey: "opacity") + self.iconNode.alpha = 0.4 + + self.textNode.layer.removeAnimation(forKey: "opacity") + self.textNode.alpha = 0.4 + } else { + self.iconNode.alpha = 1.0 + self.iconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + + self.textNode.alpha = 1.0 + self.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + @objc private func buttonPressed() { + self.action() + } + + func updateLayout(width: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = (width, sideInset, bottomInset) + let topInset: CGFloat = 8.0 + var bottomInset = bottomInset + bottomInset += topInset - (bottomInset.isZero ? 0.0 : 4.0) + + let buttonHeight: CGFloat = 40.0 + let textSize = self.textNode.updateLayout(CGSize(width: width, height: 44.0)) + + let spacing: CGFloat = 8.0 + var contentWidth = textSize.width + var contentOriginX = floorToScreenPixels((width - contentWidth) / 2.0) + if let icon = self.iconNode.image { + contentWidth += icon.size.width + spacing + contentOriginX = floorToScreenPixels((width - contentWidth) / 2.0) + transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: contentOriginX, y: 12.0 + UIScreenPixel), size: icon.size)) + contentOriginX += icon.size.width + spacing + } + let textFrame = CGRect(origin: CGPoint(x: contentOriginX, y: 17.0), size: textSize) + transition.updateFrame(node: self.textNode, frame: textFrame) + + transition.updateFrame(node: self.button, frame: textFrame.insetBy(dx: -10.0, dy: -10.0)) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) + + return topInset + buttonHeight + bottomInset + } +} + + +final class BrowserBookmarksContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = false + let blurBackground: Bool = true + + private let contentNode: ContextExtractedContentContainingNode + + init(contentNode: ContextExtractedContentContainingNode) { + self.contentNode = contentNode + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(containingItem: .node(self.contentNode), contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserContent.swift b/submodules/BrowserUI/Sources/BrowserContent.swift index 8f5f863582a..7422bed87eb 100644 --- a/submodules/BrowserUI/Sources/BrowserContent.swift +++ b/submodules/BrowserUI/Sources/BrowserContent.swift @@ -1,36 +1,77 @@ import Foundation import UIKit +import Display import ComponentFlow import SwiftSignalKit +import WebKit +import TelegramPresentationData final class BrowserContentState: Equatable { enum ContentType: Equatable { case webPage case instantPage + case document + } + + struct HistoryItem: Equatable { + let url: String + let title: String + let uuid: UUID? + let webItem: WKBackForwardListItem? + + init(url: String, title: String, uuid: UUID) { + self.url = url + self.title = title + self.uuid = uuid + self.webItem = nil + } + + init(webItem: WKBackForwardListItem) { + self.url = webItem.url.absoluteString + self.title = webItem.title ?? "" + self.uuid = nil + self.webItem = webItem + } } let title: String let url: String let estimatedProgress: Double + let readingProgress: Double let contentType: ContentType + let favicon: UIImage? + let isSecure: Bool + + let canGoBack: Bool + let canGoForward: Bool - var canGoBack: Bool - var canGoForward: Bool + let backList: [HistoryItem] + let forwardList: [HistoryItem] init( title: String, url: String, estimatedProgress: Double, + readingProgress: Double, contentType: ContentType, + favicon: UIImage? = nil, + isSecure: Bool = false, canGoBack: Bool = false, - canGoForward: Bool = false + canGoForward: Bool = false, + backList: [HistoryItem] = [], + forwardList: [HistoryItem] = [] ) { self.title = title self.url = url self.estimatedProgress = estimatedProgress + self.readingProgress = readingProgress self.contentType = contentType + self.favicon = favicon + self.isSecure = isSecure self.canGoBack = canGoBack self.canGoForward = canGoForward + self.backList = backList + self.forwardList = forwardList } static func == (lhs: BrowserContentState, rhs: BrowserContentState) -> Bool { @@ -43,49 +84,101 @@ final class BrowserContentState: Equatable { if lhs.estimatedProgress != rhs.estimatedProgress { return false } + if lhs.readingProgress != rhs.readingProgress { + return false + } if lhs.contentType != rhs.contentType { return false } + if (lhs.favicon == nil) != (rhs.favicon == nil) { + return false + } + if lhs.isSecure != rhs.isSecure { + return false + } if lhs.canGoBack != rhs.canGoBack { return false } if lhs.canGoForward != rhs.canGoForward { return false } + if lhs.backList != rhs.backList { + return false + } + if lhs.forwardList != rhs.forwardList { + return false + } return true } func withUpdatedTitle(_ title: String) -> BrowserContentState { - return BrowserContentState(title: title, url: self.url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: self.canGoForward) + return BrowserContentState(title: title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) } func withUpdatedUrl(_ url: String) -> BrowserContentState { - return BrowserContentState(title: self.title, url: url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: self.canGoForward) + return BrowserContentState(title: self.title, url: url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) + } + + func withUpdatedIsSecure(_ isSecure: Bool) -> BrowserContentState { + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) } func withUpdatedEstimatedProgress(_ estimatedProgress: Double) -> BrowserContentState { - return BrowserContentState(title: self.title, url: self.url, estimatedProgress: estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: self.canGoForward) + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) + } + + func withUpdatedReadingProgress(_ readingProgress: Double) -> BrowserContentState { + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) + } + + func withUpdatedFavicon(_ favicon: UIImage?) -> BrowserContentState { + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) } func withUpdatedCanGoBack(_ canGoBack: Bool) -> BrowserContentState { - return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: canGoBack, canGoForward: self.canGoForward) + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) } func withUpdatedCanGoForward(_ canGoForward: Bool) -> BrowserContentState { - return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: canGoForward) + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: canGoForward, backList: self.backList, forwardList: self.forwardList) + } + + func withUpdatedBackList(_ backList: [HistoryItem]) -> BrowserContentState { + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: backList, forwardList: self.forwardList) + } + + func withUpdatedForwardList(_ forwardList: [HistoryItem]) -> BrowserContentState { + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: forwardList) } } protocol BrowserContent: UIView { + var uuid: UUID { get } + + var currentState: BrowserContentState { get } var state: Signal { get } + var pushContent: (BrowserScreen.Subject) -> Void { get set } + var present: (ViewController, Any?) -> Void { get set } + var presentInGlobalOverlay: (ViewController) -> Void { get set } + var getNavigationController: () -> NavigationController? { get set } + var openAppUrl: (String) -> Void { get set } + + var minimize: () -> Void { get set } + var close: () -> Void { get set } + var onScrollingUpdate: (ContentScrollingUpdate) -> Void { get set } - + func resetScrolling() + + func reload() + func stop() + func navigateBack() func navigateForward() + func navigateTo(historyItem: BrowserContentState.HistoryItem) - func setFontSize(_ fontSize: CGFloat) - func setForceSerif(_ force: Bool) + func updatePresentationData(_ presentationData: PresentationData) + func updateFontState(_ state: BrowserPresentationState.FontState) func setSearch(_ query: String?, completion: ((Int) -> Void)?) func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) @@ -93,7 +186,11 @@ protocol BrowserContent: UIView { func scrollToTop() - func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) + func addToRecentlyVisited() + + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) + + func makeContentSnapshotView() -> UIView? } struct ContentScrollingUpdate { diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift new file mode 100644 index 00000000000..316ab28538a --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift @@ -0,0 +1,479 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramCore +import Postbox +import SwiftSignalKit +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AccountContext +import WebKit +import AppBundle +import PromptUI +import SafariServices +import ShareController +import UndoUI +import UrlEscaping + +final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { + private let context: AccountContext + private var presentationData: PresentationData + + private let webView: WKWebView + + let uuid: UUID + + private var _state: BrowserContentState + private let statePromise: Promise + + var currentState: BrowserContentState { + return self._state + } + var state: Signal { + return self.statePromise.get() + } + + var pushContent: (BrowserScreen.Subject) -> Void = { _ in } + var openAppUrl: (String) -> Void = { _ in } + var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } + var minimize: () -> Void = { } + var close: () -> Void = { } + var present: (ViewController, Any?) -> Void = { _, _ in } + var presentInGlobalOverlay: (ViewController) -> Void = { _ in } + var getNavigationController: () -> NavigationController? = { return nil } + + private var tempFile: TempBoxFile? + + init(context: AccountContext, presentationData: PresentationData, file: TelegramMediaFile) { + self.context = context + self.uuid = UUID() + self.presentationData = presentationData + + let configuration = WKWebViewConfiguration() + self.webView = WKWebView(frame: CGRect(), configuration: configuration) + self.webView.allowsLinkPreview = true + + if #available(iOS 11.0, *) { + self.webView.scrollView.contentInsetAdjustmentBehavior = .never + } + + var title: String = "file" + 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 + } + + 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.statePromise = Promise(self._state) + + super.init(frame: .zero) + + self.webView.allowsBackForwardNavigationGestures = true + self.webView.scrollView.delegate = self + self.webView.scrollView.clipsToBounds = false + self.webView.navigationDelegate = self + self.webView.uiDelegate = self + if #available(iOS 15.0, *) { + self.backgroundColor = presentationData.theme.list.plainBackgroundColor + self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor + } + self.addSubview(self.webView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + if #available(iOS 15.0, *) { + self.backgroundColor = presentationData.theme.list.plainBackgroundColor + self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor + } + if let (size, insets, fullInsets) = self.validLayout { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: .zero, transition: .immediate) + } + } + + 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 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 + } + } + } + + private var currentSearchResult: Int = 0 + private var searchResultsCount: Int = 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) + }) + } + + 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) + }) + } + + 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() { + self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true) + } + + private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets)? + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) { + self.validLayout = (size, insets, fullInsets) + + self.previousScrollingOffset = ScrollingOffsetState(value: self.webView.scrollView.contentOffset.y, isDraggingOrDecelerating: self.webView.scrollView.isDragging || self.webView.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) + + if refresh { + self.webView.reloadInputViews() + } + + 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) { + let updated = f(self._state) + self._state = updated + self.statePromise.set(.single(self._state)) + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "title" { + self.updateState { $0.withUpdatedTitle(self.webView.title ?? "") } + } else if keyPath == "URL" { + self.updateState { $0.withUpdatedUrl(self.webView.url?.absoluteString ?? "") } + self.didSetupSearch = false + } else if keyPath == "estimatedProgress" { + self.updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) } + } else if keyPath == "canGoBack" { + self.updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) } + self.webView.disablesInteractiveTransitionGestureRecognizer = self.webView.canGoBack + } else if keyPath == "canGoForward" { + self.updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) } + } + } + + private struct ScrollingOffsetState: Equatable { + var value: CGFloat + var isDraggingOrDecelerating: Bool + } + + 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) + } + + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + self.snapScrollingOffsetToInsets() + } + } + + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.snapScrollingOffsetToInsets() + } + + private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) { + let scrollView = self.webView.scrollView + let isInteracting = scrollView.isDragging || scrollView.isDecelerating + if let previousScrollingOffsetValue = self.previousScrollingOffset { + let currentBounds = scrollView.bounds + let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) + let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) + + let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue.value + self.onScrollingUpdate(ContentScrollingUpdate( + relativeOffset: relativeOffset, + absoluteOffsetToTopEdge: offsetToTopEdge, + absoluteOffsetToBottomEdge: offsetToBottomEdge, + isReset: isReset, + isInteracting: isInteracting, + transition: transition + )) + } + self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting) + + var readingProgress: CGFloat = 0.0 + if !scrollView.contentSize.height.isZero { + let value = (scrollView.contentOffset.y + scrollView.contentInset.top) / (scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.top) + readingProgress = max(0.0, min(1.0, value)) + } + self.updateState { + $0.withUpdatedReadingProgress(readingProgress) + } + } + + func resetScrolling() { + 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() { + navigationController._keepModalDismissProgress = true + self.minimize() + let controller = BrowserScreen(context: self.context, subject: subject) + navigationController._keepModalDismissProgress = true + navigationController.pushViewController(controller) + } else { + self.pushContent(subject) + } + } + + private func share(url: String) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let shareController = ShareController(context: self.context, subject: .url(url)) + shareController.actionCompleted = { [weak self] in + self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + } + self.present(shareController, nil) + } + + func addToRecentlyVisited() { + } + + func makeContentSnapshotView() -> UIView? { + return nil + } +} diff --git a/submodules/BrowserUI/Sources/BrowserExceptionDomainAlertContentNode.swift b/submodules/BrowserUI/Sources/BrowserExceptionDomainAlertContentNode.swift new file mode 100644 index 00000000000..b3f347b7a44 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserExceptionDomainAlertContentNode.swift @@ -0,0 +1,299 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import PhotoResources +import CheckNode +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 final class BrowserExceptionDomainAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + private let domain: String + + private let titleNode: ASTextNode + private let textNode: ASTextNode + + private let allowWriteCheckNode: InteractiveCheckNode + private let allowWriteLabelNode: ASTextNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var validLayout: CGSize? + + private var iconDisposable: Disposable? + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + var allowWriteAccess: Bool = true { + didSet { + self.allowWriteCheckNode.setSelected(self.allowWriteAccess, animated: true) + } + } + + init(account: Account, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, domain: String, requestWriteAccess: Bool, actions: [TextAlertAction]) { + self.strings = strings + self.domain = domain + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 0 + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 0 + + self.allowWriteCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false)) + self.allowWriteCheckNode.setSelected(true, animated: false) + self.allowWriteLabelNode = ASTextNode() + self.allowWriteLabelNode.maximumNumberOfLines = 4 + self.allowWriteLabelNode.isUserInteractionEnabled = true + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + if requestWriteAccess { + self.addSubnode(self.allowWriteCheckNode) + self.addSubnode(self.allowWriteLabelNode) + } + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.allowWriteCheckNode.valueChanged = { [weak self] value in + if let strongSelf = self { + strongSelf.allowWriteAccess = !strongSelf.allowWriteAccess + } + } + + self.updateTheme(theme) + } + + deinit { + self.iconDisposable?.dispose() + } + + override func didLoad() { + super.didLoad() + + self.allowWriteLabelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.allowWriteTap(_:)))) + } + + @objc private func allowWriteTap(_ gestureRecognizer: UITapGestureRecognizer) { + if self.allowWriteCheckNode.isUserInteractionEnabled { + self.allowWriteAccess = !self.allowWriteAccess + } + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: "Open in Browser", font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: "Do you want to open this link in your default browser?", font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) + + self.allowWriteLabelNode.attributedText = formattedText("Always open links from **\(self.domain)** in browser", color: theme.primaryColor) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width , 270.0) + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + + let titleSize = self.titleNode.measure(size) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 13.0 + + let textSize = self.textNode.measure(size) + var textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize) + origin.y += textSize.height + + var entriesHeight: CGFloat = 0.0 + + if self.allowWriteLabelNode.supernode != nil { + origin.y += 16.0 + entriesHeight += 16.0 + + let checkSize = CGSize(width: 22.0, height: 22.0) + let condensedSize = CGSize(width: size.width - 76.0, height: size.height) + + let allowWriteSize = self.allowWriteLabelNode.measure(condensedSize) + transition.updateFrame(node: self.allowWriteLabelNode, frame: CGRect(origin: CGPoint(x: 46.0, y: origin.y), size: allowWriteSize)) + transition.updateFrame(node: self.allowWriteCheckNode, frame: CGRect(origin: CGPoint(x: 12.0, y: origin.y - 2.0), size: checkSize)) + origin.y += allowWriteSize.height + entriesHeight += allowWriteSize.height + } + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + var contentWidth = max(textSize.width, minActionsWidth) + contentWidth = max(contentWidth, 234.0) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultWidth = contentWidth + insets.left + insets.right + let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + entriesHeight + actionsHeight + 17.0 + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + textFrame.origin.x = floorToScreenPixels((resultSize.width - textFrame.width) / 2.0) + transition.updateFrame(node: self.textNode, frame: textFrame) + + return resultSize + } +} + +public func browserExceptionDomainAlertController(context: AccountContext, domain: String, completion: @escaping (Bool) -> Void) -> AlertController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let theme = presentationData.theme + let strings = presentationData.strings + + var dismissImpl: ((Bool) -> Void)? + var getContentNodeImpl: (() -> BrowserExceptionDomainAlertContentNode?)? + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + }), TextAlertAction(type: .defaultAction, title: "Continue", action: { + if let allowWriteAccess = getContentNodeImpl?()?.allowWriteAccess { + completion(allowWriteAccess) + } else { + completion(false) + } + dismissImpl?(true) + })] + + let contentNode = BrowserExceptionDomainAlertContentNode(account: context.account, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, domain: domain, requestWriteAccess: true, actions: actions) + getContentNodeImpl = { [weak contentNode] in + return contentNode + } + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + dismissImpl = { [weak controller] animated in + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index e0004568688..f334322e608 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -1,957 +1,1434 @@ -//import Foundation -//import UIKit -//import AsyncDisplayKit -//import TelegramCore -//import Postbox -//import SwiftSignalKit -//import Display -//import ComponentFlow -//import TelegramPresentationData -//import TelegramUIPreferences -//import AccountContext -//import AppBundle -//import InstantPageUI -// -//final class InstantPageView: UIView, UIScrollViewDelegate { -// private let webPage: TelegramMediaWebpage -// private var initialAnchor: String? -// private var pendingAnchor: String? -// private var initialState: InstantPageStoredState? -// -// private let scrollNode: ASScrollNode -// private let scrollNodeHeader: ASDisplayNode -// private let scrollNodeFooter: ASDisplayNode -// private var linkHighlightingNode: LinkHighlightingNode? -// private var textSelectionNode: LinkHighlightingNode? -// -// var currentLayout: InstantPageLayout? -// var currentLayoutTiles: [InstantPageTile] = [] -// var currentLayoutItemsWithNodes: [InstantPageItem] = [] -// var distanceThresholdGroupCount: [Int: Int] = [:] -// -// var visibleTiles: [Int: InstantPageTileNode] = [:] -// var visibleItemsWithNodes: [Int: InstantPageNode] = [:] -// -// var currentWebEmbedHeights: [Int : CGFloat] = [:] -// var currentExpandedDetails: [Int : Bool]? -// var currentDetailsItems: [InstantPageDetailsItem] = [] -// -// var currentAccessibilityAreas: [AccessibilityAreaNode] = [] -// -// init(webPage: TelegramMediaWebpage) { -// self.webPage = webPage -// -// self.scrollNode = ASScrollNode() -// -// super.init(frame: frame) -// -// self.addSubview(self.scrollNode.view) -// -// self.scrollNode.view.delaysContentTouches = false -// self.scrollNode.view.delegate = self -// -// if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { -// self.scrollNode.view.contentInsetAdjustmentBehavior = .never -// } -// -// let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) -// recognizer.delaysTouchesBegan = false -// recognizer.tapActionAtPoint = { [weak self] point in -// if let strongSelf = self { -// return strongSelf.tapActionAtPoint(point) -// } -// return .waitForSingleTap -// } -// recognizer.highlight = { [weak self] point in -// if let strongSelf = self { -// strongSelf.updateTouchesAtPoint(point) -// } -// } -// self.scrollNode.view.addGestureRecognizer(recognizer) -// } -// -// required init?(coder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -// -// func tapActionAtPoint(_ point: CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction { -// if let currentLayout = self.currentLayout { -// for item in currentLayout.items { -// let frame = self.effectiveFrameForItem(item) -// if frame.contains(point) { -// if item is InstantPagePeerReferenceItem { -// return .fail -// } else if item is InstantPageAudioItem { -// return .fail -// } else if item is InstantPageArticleItem { -// return .fail -// } else if item is InstantPageFeedbackItem { -// return .fail -// } else if let item = item as? InstantPageDetailsItem { -// for (_, itemNode) in self.visibleItemsWithNodes { -// if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item { -// return itemNode.tapActionAtPoint(point.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY)) -// } -// } -// } -// if !(item is InstantPageImageItem || item is InstantPagePlayableVideoItem) { -// break -// } -// } -// } -// } -// return .waitForSingleTap -// } -// -// private func updateTouchesAtPoint(_ location: CGPoint?) { -// var rects: [CGRect]? -// if let location = location, let currentLayout = self.currentLayout { -// for item in currentLayout.items { -// let itemFrame = self.effectiveFrameForItem(item) -// if itemFrame.contains(location) { -// var contentOffset = CGPoint() -// if let item = item as? InstantPageScrollableItem { -// contentOffset = self.scrollableContentOffset(item: item) -// } -// var itemRects = item.linkSelectionRects(at: location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY)) -// -// for i in 0 ..< itemRects.count { -// itemRects[i] = itemRects[i].offsetBy(dx: itemFrame.minX - contentOffset.x, dy: itemFrame.minY).insetBy(dx: -2.0, dy: -2.0) -// } -// if !itemRects.isEmpty { -// rects = itemRects -// break -// } -// } -// } -// } -// -// if let rects = rects { -// let linkHighlightingNode: LinkHighlightingNode -// if let current = self.linkHighlightingNode { -// linkHighlightingNode = current -// } else { -// let highlightColor = self.theme?.linkHighlightColor ?? UIColor(rgb: 0x007aff).withAlphaComponent(0.4) -// linkHighlightingNode = LinkHighlightingNode(color: highlightColor) -// linkHighlightingNode.isUserInteractionEnabled = false -// self.linkHighlightingNode = linkHighlightingNode -// self.scrollNode.addSubnode(linkHighlightingNode) -// } -// linkHighlightingNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size) -// linkHighlightingNode.updateRects(rects) -// } else if let linkHighlightingNode = self.linkHighlightingNode { -// self.linkHighlightingNode = nil -// linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in -// linkHighlightingNode?.removeFromSupernode() -// }) -// } -// } -// -// private func updatePageLayout() { -// guard let containerLayout = self.containerLayout, let webPage = self.webPage, let theme = self.theme else { -// return -// } -// -// let currentLayout = instantPageLayoutForWebPage(webPage, userLocation: self.sourceLocation.userLocation, boundingWidth: containerLayout.size.width, safeInset: containerLayout.safeInsets.left, strings: self.strings, theme: theme, dateTimeFormat: self.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights) -// -// for (_, tileNode) in self.visibleTiles { -// tileNode.removeFromSupernode() -// } -// self.visibleTiles.removeAll() -// -// let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: containerLayout.size.width) -// -// var currentDetailsItems: [InstantPageDetailsItem] = [] -// var currentLayoutItemsWithNodes: [InstantPageItem] = [] -// var distanceThresholdGroupCount: [Int : Int] = [:] -// -// var expandedDetails: [Int : Bool] = [:] -// -// var detailsIndex = -1 -// for item in currentLayout.items { -// if item.wantsNode { -// currentLayoutItemsWithNodes.append(item) -// if let group = item.distanceThresholdGroup() { -// let count: Int -// if let currentCount = distanceThresholdGroupCount[Int(group)] { -// count = currentCount -// } else { -// count = 0 -// } -// distanceThresholdGroupCount[Int(group)] = count + 1 -// } -// if let detailsItem = item as? InstantPageDetailsItem { -// detailsIndex += 1 -// expandedDetails[detailsIndex] = detailsItem.initiallyExpanded -// currentDetailsItems.append(detailsItem) -// } -// } -// } -// -// if var currentExpandedDetails = self.currentExpandedDetails { -// for (index, expanded) in expandedDetails { -// if currentExpandedDetails[index] == nil { -// currentExpandedDetails[index] = expanded -// } -// } -// self.currentExpandedDetails = currentExpandedDetails -// } else { -// self.currentExpandedDetails = expandedDetails -// } -// -// let accessibilityAreas = instantPageAccessibilityAreasFromLayout(currentLayout, boundingWidth: containerLayout.size.width) -// -// self.currentLayout = currentLayout -// self.currentLayoutTiles = currentLayoutTiles -// self.currentLayoutItemsWithNodes = currentLayoutItemsWithNodes -// self.currentDetailsItems = currentDetailsItems -// self.distanceThresholdGroupCount = distanceThresholdGroupCount -// -// for areaNode in self.currentAccessibilityAreas { -// areaNode.removeFromSupernode() -// } -// for areaNode in accessibilityAreas { -// self.scrollNode.addSubnode(areaNode) -// } -// self.currentAccessibilityAreas = accessibilityAreas -// -// self.scrollNode.view.contentSize = currentLayout.contentSize -// self.scrollNodeFooter.frame = CGRect(origin: CGPoint(x: 0.0, y: currentLayout.contentSize.height), size: CGSize(width: containerLayout.size.width, height: 2000.0)) -// } -// -// func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) { -// guard let theme = self.theme else { -// return -// } -// -// var visibleTileIndices = Set() -// var visibleItemIndices = Set() -// -// var topNode: ASDisplayNode? -// let topTileNode = topNode -// if let scrollSubnodes = self.scrollNode.subnodes { -// for node in scrollSubnodes.reversed() { -// if let node = node as? InstantPageTileNode { -// topNode = node -// break -// } -// } -// } -// -// var collapseOffset: CGFloat = 0.0 -// let transition: ContainedViewLayoutTransition -// if animated { -// transition = .animated(duration: 0.3, curve: .spring) -// } else { -// transition = .immediate -// } -// -// var itemIndex = -1 -// var embedIndex = -1 -// var detailsIndex = -1 -// -// var previousDetailsNode: InstantPageDetailsNode? -// -// for item in self.currentLayoutItemsWithNodes { -// itemIndex += 1 -// if item is InstantPageWebEmbedItem { -// embedIndex += 1 -// } -// if let imageItem = item as? InstantPageImageItem, imageItem.media.media is TelegramMediaWebpage { -// embedIndex += 1 -// } -// if item is InstantPageDetailsItem { -// detailsIndex += 1 -// } -// -// var itemThreshold: CGFloat = 0.0 -// if let group = item.distanceThresholdGroup() { -// var count: Int = 0 -// if let currentCount = self.distanceThresholdGroupCount[group] { -// count = currentCount -// } -// itemThreshold = item.distanceThresholdWithGroupCount(count) -// } -// -// var itemFrame = item.frame.offsetBy(dx: 0.0, dy: -collapseOffset) -// var thresholdedItemFrame = itemFrame -// thresholdedItemFrame.origin.y -= itemThreshold -// thresholdedItemFrame.size.height += itemThreshold * 2.0 -// -// if let detailsItem = item as? InstantPageDetailsItem, let expanded = self.currentExpandedDetails?[detailsIndex] { -// let height = expanded ? self.effectiveSizeForDetails(detailsItem).height : detailsItem.titleHeight -// collapseOffset += itemFrame.height - height -// itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: height)) -// } -// -// if visibleBounds.intersects(thresholdedItemFrame) { -// visibleItemIndices.insert(itemIndex) -// -// var itemNode = self.visibleItemsWithNodes[itemIndex] -// if let currentItemNode = itemNode { -// if !item.matchesNode(currentItemNode) { -// currentItemNode.removeFromSupernode() -// self.visibleItemsWithNodes.removeValue(forKey: itemIndex) -// itemNode = nil -// } -// } -// -// if itemNode == nil { -// let itemIndex = itemIndex -// let embedIndex = embedIndex -// let detailsIndex = detailsIndex -// if let newNode = item.node(context: self.context, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, theme: theme, sourceLocation: self.sourceLocation, openMedia: { [weak self] media in -// self?.openMedia(media) -// }, longPressMedia: { [weak self] media in -// self?.longPressMedia(media) -// }, activatePinchPreview: { [weak self] sourceNode in -// guard let strongSelf = self, let controller = strongSelf.controller else { -// return -// } -// let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { -// guard let strongSelf = self else { -// return CGRect() -// } -// -// let localRect = CGRect(origin: CGPoint(x: 0.0, y: strongSelf.navigationBar.frame.maxY), size: CGSize(width: strongSelf.bounds.width, height: strongSelf.bounds.height - strongSelf.navigationBar.frame.maxY)) -// return strongSelf.view.convert(localRect, to: nil) -// }) -// controller.window?.presentInGlobalOverlay(pinchController) -// }, pinchPreviewFinished: { [weak self] itemNode in -// guard let strongSelf = self else { -// return -// } -// for (_, listItemNode) in strongSelf.visibleItemsWithNodes { -// if let listItemNode = listItemNode as? InstantPagePeerReferenceNode { -// if listItemNode.frame.intersects(itemNode.frame) && listItemNode.frame.maxY <= itemNode.frame.maxY + 2.0 { -// listItemNode.layer.animateAlpha(from: 0.0, to: listItemNode.alpha, duration: 0.25) -// break -// } -// } -// } -// }, openPeer: { [weak self] peerId in -// self?.openPeer(peerId) -// }, openUrl: { [weak self] url in -// self?.openUrl(url) -// }, updateWebEmbedHeight: { [weak self] height in -// self?.updateWebEmbedHeight(embedIndex, height) -// }, updateDetailsExpanded: { [weak self] expanded in -// self?.updateDetailsExpanded(detailsIndex, expanded) -// }, currentExpandedDetails: self.currentExpandedDetails) { -// newNode.frame = itemFrame -// newNode.updateLayout(size: itemFrame.size, transition: transition) -// if let topNode = topNode { -// self.scrollNode.insertSubnode(newNode, aboveSubnode: topNode) -// } else { -// self.scrollNode.insertSubnode(newNode, at: 0) -// } -// topNode = newNode -// self.visibleItemsWithNodes[itemIndex] = newNode -// itemNode = newNode -// -// if let itemNode = itemNode as? InstantPageDetailsNode { -// itemNode.requestLayoutUpdate = { [weak self] animated in -// if let strongSelf = self { -// strongSelf.updateVisibleItems(visibleBounds: strongSelf.scrollNode.view.bounds, animated: animated) -// } -// } -// -// if let previousDetailsNode = previousDetailsNode { -// if itemNode.frame.minY - previousDetailsNode.frame.maxY < 1.0 { -// itemNode.previousNode = previousDetailsNode -// } -// } -// previousDetailsNode = itemNode -// } -// } -// } else { -// if let itemNode = itemNode, itemNode.frame != itemFrame { -// transition.updateFrame(node: itemNode, frame: itemFrame) -// itemNode.updateLayout(size: itemFrame.size, transition: transition) -// } -// } -// -// if let itemNode = itemNode as? InstantPageDetailsNode { -// itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated) -// } -// } -// } -// -// topNode = topTileNode -// -// var tileIndex = -1 -// for tile in self.currentLayoutTiles { -// tileIndex += 1 -// -// let tileFrame = effectiveFrameForTile(tile) -// var tileVisibleFrame = tileFrame -// tileVisibleFrame.origin.y -= 400.0 -// tileVisibleFrame.size.height += 400.0 * 2.0 -// if tileVisibleFrame.intersects(visibleBounds) { -// visibleTileIndices.insert(tileIndex) -// -// if self.visibleTiles[tileIndex] == nil { -// let tileNode = InstantPageTileNode(tile: tile, backgroundColor: theme.pageBackgroundColor) -// tileNode.frame = tileFrame -// if let topNode = topNode { -// self.scrollNode.insertSubnode(tileNode, aboveSubnode: topNode) -// } else { -// self.scrollNode.insertSubnode(tileNode, at: 0) -// } -// topNode = tileNode -// self.visibleTiles[tileIndex] = tileNode -// } else { -// if visibleTiles[tileIndex]!.frame != tileFrame { -// transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame) -// } -// } -// } -// } -// -// if let currentLayout = self.currentLayout { -// let effectiveContentHeight = currentLayout.contentSize.height - collapseOffset -// if effectiveContentHeight != self.scrollNode.view.contentSize.height { -// transition.animateView { -// self.scrollNode.view.contentSize = CGSize(width: currentLayout.contentSize.width, height: effectiveContentHeight) -// } -// let previousFrame = self.scrollNodeFooter.frame -// self.scrollNodeFooter.frame = CGRect(origin: CGPoint(x: 0.0, y: effectiveContentHeight), size: CGSize(width: previousFrame.width, height: 2000.0)) -// transition.animateFrame(node: self.scrollNodeFooter, from: previousFrame) -// } -// } -// -// var removeTileIndices: [Int] = [] -// for (index, tileNode) in self.visibleTiles { -// if !visibleTileIndices.contains(index) { -// removeTileIndices.append(index) -// tileNode.removeFromSupernode() -// } -// } -// for index in removeTileIndices { -// self.visibleTiles.removeValue(forKey: index) -// } -// -// var removeItemIndices: [Int] = [] -// for (index, itemNode) in self.visibleItemsWithNodes { -// if !visibleItemIndices.contains(index) { -// removeItemIndices.append(index) -// itemNode.removeFromSupernode() -// } else { -// var itemFrame = itemNode.frame -// let itemThreshold: CGFloat = 200.0 -// itemFrame.origin.y -= itemThreshold -// itemFrame.size.height += itemThreshold * 2.0 -// itemNode.updateIsVisible(visibleBounds.intersects(itemFrame)) -// } -// } -// for index in removeItemIndices { -// self.visibleItemsWithNodes.removeValue(forKey: index) -// } -// } -// -// func scrollViewDidScroll(_ scrollView: UIScrollView) { -// self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) -// self.previousContentOffset = self.scrollNode.view.contentOffset -// } -// -// func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { -// self.isDeceleratingBecauseOfDragging = decelerate -// if !decelerate { -// self.updateNavigationBar(forceState: true) -// } -// } -// -// func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { -// self.isDeceleratingBecauseOfDragging = false -// } -// -// private func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint { -// var contentOffset = CGPoint() -// for (_, itemNode) in self.visibleItemsWithNodes { -// if let itemNode = itemNode as? InstantPageScrollableNode, itemNode.item === item { -// contentOffset = itemNode.contentOffset -// break -// } -// } -// return contentOffset -// } -// -// private func nodeForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsNode? { -// for (_, itemNode) in self.visibleItemsWithNodes { -// if let detailsNode = itemNode as? InstantPageDetailsNode, detailsNode.item === item { -// return detailsNode -// } -// } -// return nil -// } -// -// private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize { -// if let node = nodeForDetailsItem(item) { -// return CGSize(width: item.frame.width, height: node.effectiveContentSize.height + item.titleHeight) -// } else { -// return item.frame.size -// } -// } -// -// private func effectiveFrameForTile(_ tile: InstantPageTile) -> CGRect { -// let layoutOrigin = tile.frame.origin -// var origin = layoutOrigin -// for item in self.currentDetailsItems { -// let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded -// if layoutOrigin.y >= item.frame.maxY { -// let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight -// origin.y += height - item.frame.height -// } -// } -// return CGRect(origin: origin, size: tile.frame.size) -// } -// -// private func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect { -// let layoutOrigin = item.frame.origin -// var origin = layoutOrigin -// -// for item in self.currentDetailsItems { -// let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded -// if layoutOrigin.y >= item.frame.maxY { -// let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight -// origin.y += height - item.frame.height -// } -// } -// -// if let item = item as? InstantPageDetailsItem { -// let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded -// let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight -// return CGRect(origin: origin, size: CGSize(width: item.frame.width, height: height)) -// } else { -// return CGRect(origin: origin, size: item.frame.size) -// } -// } -// -// private func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? { -// if let currentLayout = self.currentLayout { -// for item in currentLayout.items { -// let itemFrame = self.effectiveFrameForItem(item) -// if itemFrame.contains(location) { -// if let item = item as? InstantPageTextItem, item.selectable { -// return (item, CGPoint(x: itemFrame.minX - item.frame.minX, y: itemFrame.minY - item.frame.minY)) -// } else if let item = item as? InstantPageScrollableItem { -// let contentOffset = scrollableContentOffset(item: item) -// if let (textItem, parentOffset) = item.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY)) { -// return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x - contentOffset.x, dy: parentOffset.y)) -// } -// } else if let item = item as? InstantPageDetailsItem { -// for (_, itemNode) in self.visibleItemsWithNodes { -// if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item { -// if let (textItem, parentOffset) = itemNode.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX, dy: -itemFrame.minY)) { -// return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x, dy: parentOffset.y)) -// } -// } -// } -// } -// } -// } -// } -// return nil -// } -// -// private func urlForTapLocation(_ location: CGPoint) -> InstantPageUrlItem? { -// if let (item, parentOffset) = self.textItemAtLocation(location) { -// return item.urlAttribute(at: location.offsetBy(dx: -item.frame.minX - parentOffset.x, dy: -item.frame.minY - parentOffset.y)) -// } -// return nil -// } -// -// private func longPressMedia(_ media: InstantPageMedia) { -// let controller = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in -// if let strongSelf = self, let image = media.media as? TelegramMediaImage { -// let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) -// let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() -// } -// }), ContextMenuAction(content: .text(title: self.strings.Conversation_LinkDialogSave, accessibilityLabel: self.strings.Conversation_LinkDialogSave), action: { [weak self] in -// if let strongSelf = self, let image = media.media as? TelegramMediaImage { -// let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) -// let _ = saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() -// } -// }), ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in -// if let strongSelf = self, let webPage = strongSelf.webPage, let image = media.media as? TelegramMediaImage { -// strongSelf.present(ShareController(context: strongSelf.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil) -// } -// })], catchTapsOutside: true) -// self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in -// if let strongSelf = self { -// for (_, itemNode) in strongSelf.visibleItemsWithNodes { -// if let (node, _, _) = itemNode.transitionNode(media: media) { -// return (strongSelf.scrollNode, node.convert(node.bounds, to: strongSelf.scrollNode), strongSelf, strongSelf.bounds) -// } -// } -// } -// return nil -// })) -// } -// -// @objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { -// switch recognizer.state { -// case .ended: -// if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { -// switch gesture { -// case .tap: -// break -//// if let url = self.urlForTapLocation(location) { -//// self.openUrl(url) -//// } -// case .longTap: -// break -//// if let theme = self.theme, let url = self.urlForTapLocation(location) { -//// let canOpenIn = availableOpenInOptions(context: self.context, item: .url(url: url.url)).count > 1 -//// let openText = canOpenIn ? self.strings.Conversation_FileOpenIn : self.strings.Conversation_LinkDialogOpen -//// let actionSheet = ActionSheetController(instantPageTheme: theme) -//// actionSheet.setItemGroups([ActionSheetItemGroup(items: [ -//// ActionSheetTextItem(title: url.url), -//// ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in -//// actionSheet?.dismissAnimated() -//// if let strongSelf = self { -//// if canOpenIn { -//// strongSelf.openUrlIn(url) -//// } else { -//// strongSelf.openUrl(url) -//// } -//// } -//// }), -//// ActionSheetButtonItem(title: self.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in -//// actionSheet?.dismissAnimated() -//// UIPasteboard.general.string = url.url -//// }), -//// ActionSheetButtonItem(title: self.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in -//// actionSheet?.dismissAnimated() -//// if let link = URL(string: url.url) { -//// let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) -//// } -//// }) -//// ]), ActionSheetItemGroup(items: [ -//// ActionSheetButtonItem(title: self.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in -//// actionSheet?.dismissAnimated() -//// }) -//// ])]) -//// self.present(actionSheet, nil) -//// } else if let (item, parentOffset) = self.textItemAtLocation(location) { -//// let textFrame = item.frame -//// var itemRects = item.lineRects() -//// for i in 0 ..< itemRects.count { -//// itemRects[i] = itemRects[i].offsetBy(dx: parentOffset.x + textFrame.minX, dy: parentOffset.y + textFrame.minY).insetBy(dx: -2.0, dy: -2.0) -//// } -//// self.updateTextSelectionRects(itemRects, text: item.plainText()) -//// } -// default: -// break -// } -// } -// default: -// break -// } -// } -// -// private func updateTextSelectionRects(_ rects: [CGRect], text: String?) { -// if let text = text, !rects.isEmpty { -// let textSelectionNode: LinkHighlightingNode -// if let current = self.textSelectionNode { -// textSelectionNode = current -// } else { -// textSelectionNode = LinkHighlightingNode(color: UIColor.lightGray.withAlphaComponent(0.4)) -// textSelectionNode.isUserInteractionEnabled = false -// self.textSelectionNode = textSelectionNode -// self.scrollNode.addSubnode(textSelectionNode) -// } -// textSelectionNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size) -// textSelectionNode.updateRects(rects) -// -// var coveringRect = rects[0] -// for i in 1 ..< rects.count { -// coveringRect = coveringRect.union(rects[i]) -// } -// -// let context = self.context -// let strings = self.strings -// let _ = (context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) -// |> take(1) -// |> deliverOnMainQueue).start(next: { [weak self] sharedData in -// let translationSettings: TranslationSettings -// if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) { -// translationSettings = current -// } else { -// translationSettings = TranslationSettings.defaultSettings -// } -// -// var actions: [ContextMenuAction] = [ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuCopy, accessibilityLabel: strings.Conversation_ContextMenuCopy), action: { [weak self] in -// UIPasteboard.general.string = text -// -// if let strongSelf = self { -// let presentationData = context.sharedContext.currentPresentationData.with { $0 } -// strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) -// } -// }), ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuShare, accessibilityLabel: strings.Conversation_ContextMenuShare), action: { [weak self] in -// if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { -// strongSelf.present(ShareController(context: strongSelf.context, subject: .quote(text: text, url: content.url)), nil) -// } -// })] -// -// let (canTranslate, language) = canTranslateText(context: context, text: text, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: false, ignoredLanguages: translationSettings.ignoredLanguages) -// if canTranslate { -// actions.append(ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuTranslate, accessibilityLabel: strings.Conversation_ContextMenuTranslate), action: { [weak self] in -// let controller = TranslateScreen(context: context, text: text, canCopy: true, fromLanguage: language) -// controller.pushController = { [weak self] c in -// (self?.controller?.navigationController as? NavigationController)?._keepModalDismissProgress = true -// self?.controller?.push(c) -// } -// controller.presentController = { [weak self] c in -// self?.controller?.present(c, in: .window(.root)) -// } -// self?.present(controller, nil) -// })) -// } -// -// let controller = makeContextMenuController(actions: actions) -// controller.dismissed = { [weak self] in -// self?.updateTextSelectionRects([], text: nil) -// } -// self?.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in -// if let strongSelf = self { -// return (strongSelf.scrollNode, coveringRect.insetBy(dx: -3.0, dy: -3.0), strongSelf, strongSelf.bounds) -// } else { -// return nil -// } -// })) -// }) -// -// textSelectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) -// } else if let textSelectionNode = self.textSelectionNode { -// self.textSelectionNode = nil -// textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in -// textSelectionNode?.removeFromSupernode() -// }) -// } -// } -// -// private func findAnchorItem(_ anchor: String, items: [InstantPageItem]) -> (InstantPageItem, CGFloat, Bool, [InstantPageDetailsItem])? { -// for item in items { -// if let item = item as? InstantPageAnchorItem, item.anchor == anchor { -// return (item, -10.0, false, []) -// } else if let item = item as? InstantPageTextItem { -// if let (lineIndex, empty) = item.anchors[anchor] { -// return (item, item.lines[lineIndex].frame.minY - 10.0, !empty, []) -// } -// } -// else if let item = item as? InstantPageTableItem { -// if let (offset, empty) = item.anchors[anchor] { -// return (item, offset - 10.0, !empty, []) -// } -// } -// else if let item = item as? InstantPageDetailsItem { -// if let (foundItem, offset, reference, detailsItems) = self.findAnchorItem(anchor, items: item.items) { -// var detailsItems = detailsItems -// detailsItems.insert(item, at: 0) -// return (foundItem, offset, reference, detailsItems) -// } -// } -// } -// return nil -// } -// -// private func presentReferenceView(item: InstantPageTextItem, referenceAnchor: String) { -//// guard let theme = self.theme, let webPage = self.webPage else { -//// return -//// } -//// -//// var targetAnchor: InstantPageTextAnchorItem? -//// for (name, (line, _)) in item.anchors { -//// if name == referenceAnchor { -//// let anchors = item.lines[line].anchorItems -//// for anchor in anchors { -//// if anchor.name == referenceAnchor { -//// targetAnchor = anchor -//// break -//// } -//// } -//// } -//// } -//// -//// guard let anchorText = targetAnchor?.anchorText else { -//// return -//// } -//// -//// let controller = InstantPageReferenceController(context: self.context, sourceLocation: self.sourceLocation, theme: theme, webPage: webPage, anchorText: anchorText, openUrl: { [weak self] url in -//// self?.openUrl(url) -//// }, openUrlIn: { [weak self] url in -//// self?.openUrlIn(url) -//// }, present: { [weak self] c, a in -//// self?.present(c, a) -//// }) -//// self.present(controller, nil) -// } -// -// private func scrollToAnchor(_ anchor: String) { -// guard let items = self.currentLayout?.items else { -// return -// } -// -// if !anchor.isEmpty { -// if let (item, lineOffset, reference, detailsItems) = findAnchorItem(String(anchor), items: items) { -// if let item = item as? InstantPageTextItem, reference { -// self.presentReferenceView(item: item, referenceAnchor: anchor) -// } else { -// var previousDetailsNode: InstantPageDetailsNode? -// var containerOffset: CGFloat = 0.0 -// for detailsItem in detailsItems { -// if let previousNode = previousDetailsNode { -// previousNode.contentNode.updateDetailsExpanded(detailsItem.index, true, animated: false) -// let frame = previousNode.effectiveFrameForItem(detailsItem) -// containerOffset += frame.minY -// -// previousDetailsNode = previousNode.contentNode.nodeForDetailsItem(detailsItem) -// previousDetailsNode?.setExpanded(true, animated: false) -// } else { -// self.updateDetailsExpanded(detailsItem.index, true, animated: false) -// let frame = self.effectiveFrameForItem(detailsItem) -// containerOffset += frame.minY -// -// previousDetailsNode = self.nodeForDetailsItem(detailsItem) -// previousDetailsNode?.setExpanded(true, animated: false) -// } -// } -// -// let frame: CGRect -// if let previousDetailsNode = previousDetailsNode { -// frame = previousDetailsNode.effectiveFrameForItem(item) -// } else { -// frame = self.effectiveFrameForItem(item) -// } -// -// var targetY = min(containerOffset + frame.minY + lineOffset, self.scrollNode.view.contentSize.height - self.scrollNode.frame.height) -// if targetY < self.scrollNode.view.contentOffset.y { -// targetY -= self.scrollNode.view.contentInset.top -// } else { -// targetY -= self.containerLayout?.statusBarHeight ?? 20.0 -// } -// self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: targetY), animated: true) -// } -// } else if let webPage = self.webPage, case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, !instantPage.isComplete { +import Foundation +import UIKit +import AsyncDisplayKit +import TelegramCore +import Postbox +import SwiftSignalKit +import Display +import ComponentFlow +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import InstantPageUI +import UndoUI +import TranslateUI +import ContextUI +import Pasteboard +import SaveToCameraRoll +import ShareController +import SafariServices +import LocationUI +import OpenInExternalAppUI +import GalleryUI + +final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDelegate { + private let context: AccountContext + private var presentationData: PresentationData + private var theme: InstantPageTheme + private var settings: InstantPagePresentationSettings = .defaultSettings + private let sourceLocation: InstantPageSourceLocation + + private var webPage: TelegramMediaWebpage? + + let uuid: UUID + + var currentState: BrowserContentState { + return self._state + } + private var _state: BrowserContentState + private let statePromise: Promise + var state: Signal { + return self.statePromise.get() + } + + private var initialAnchor: String? + private var pendingAnchor: String? + private var initialState: InstantPageStoredState? + + private let wrapperNode: ASDisplayNode + fileprivate let scrollNode: ASScrollNode + private let scrollNodeFooter: ASDisplayNode + private var linkHighlightingNode: LinkHighlightingNode? + private var textSelectionNode: LinkHighlightingNode? + + var currentLayout: InstantPageLayout? + var currentLayoutTiles: [InstantPageTile] = [] + var currentLayoutItemsWithNodes: [InstantPageItem] = [] + var distanceThresholdGroupCount: [Int: Int] = [:] + + var visibleTiles: [Int: InstantPageTileNode] = [:] + var visibleItemsWithNodes: [Int: InstantPageNode] = [:] + + var currentWebEmbedHeights: [Int : CGFloat] = [:] + var currentExpandedDetails: [Int : Bool]? + var currentDetailsItems: [InstantPageDetailsItem] = [] + + var currentAccessibilityAreas: [AccessibilityAreaNode] = [] + + var pushContent: (BrowserScreen.Subject) -> Void = { _ in } + var openAppUrl: (String) -> Void = { _ in } + var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } + var minimize: () -> Void = { } + var close: () -> Void = { } + + var openPeer: (EnginePeer) -> Void = { _ in } + + var present: (ViewController, Any?) -> Void = { _, _ in } + var presentInGlobalOverlay: (ViewController) -> Void = { _ in } + var push: (ViewController) -> Void = { _ in } + var getNavigationController: () -> NavigationController? = { return nil } + + private var webpageDisposable: Disposable? + private let hiddenMediaDisposable = MetaDisposable() + private let loadWebpageDisposable = MetaDisposable() + private let resolveUrlDisposable = MetaDisposable() + private let updateLayoutDisposable = MetaDisposable() + + private let loadProgress = ValuePromise(1.0, ignoreRepeated: true) + private let readingProgress = ValuePromise(1.0, ignoreRepeated: true) + + private var containerLayout: (size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets)? + private var setupScrollOffsetOnLayout = false + + init(context: AccountContext, presentationData: PresentationData, webPage: TelegramMediaWebpage, anchor: String?, url: String, sourceLocation: InstantPageSourceLocation) { + self.context = context + self.webPage = webPage + self.presentationData = presentationData + self.theme = instantPageThemeForType(presentationData.theme.overallDarkAppearance ? .dark : .light, settings: .defaultSettings) + self.sourceLocation = sourceLocation + + self.uuid = UUID() + + let title: String + if case let .Loaded(content) = webPage.content { + title = content.title ?? "" + } else { + title = "" + } + + self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .instantPage) + self.statePromise = Promise(self._state) + + self.wrapperNode = ASDisplayNode() + self.scrollNode = ASScrollNode() + self.scrollNode.backgroundColor = self.theme.pageBackgroundColor + + self.scrollNodeFooter = ASDisplayNode() + self.scrollNodeFooter.backgroundColor = self.theme.panelBackgroundColor + + super.init(frame: .zero) + + self.statePromise.set(.single(self._state) + |> then( + combineLatest( + self.loadProgress.get(), + self.readingProgress.get() + ) + |> map { estimatedProgress, readingProgress in + return BrowserContentState(title: title, url: url, estimatedProgress: estimatedProgress, readingProgress: readingProgress, contentType: .instantPage) + } + )) + + self.addSubnode(self.wrapperNode) + self.wrapperNode.addSubnode(self.scrollNode) + self.scrollNode.addSubnode(self.scrollNodeFooter) + + self.scrollNode.view.delaysContentTouches = false + self.scrollNode.view.delegate = self + + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + recognizer.delaysTouchesBegan = false + recognizer.tapActionAtPoint = { [weak self] point in + if let strongSelf = self { + return strongSelf.tapActionAtPoint(point) + } + return .waitForSingleTap + } + recognizer.highlight = { [weak self] point in + if let strongSelf = self { + strongSelf.updateTouchesAtPoint(point) + } + } + self.scrollNode.view.addGestureRecognizer(recognizer) + + self.webpageDisposable = (actualizedWebpage(account: context.account, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + self.webPage = result + self.updateWebPage(result, anchor: self.initialAnchor) + }) + } + + deinit { + self.webpageDisposable?.dispose() + self.hiddenMediaDisposable.dispose() + self.loadWebpageDisposable.dispose() + self.resolveUrlDisposable.dispose() + self.updateLayoutDisposable.dispose() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + + self.theme = instantPageThemeForType(presentationData.theme.overallDarkAppearance ? .dark : .light, settings: self.settings) + self.updatePageLayout() + self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) + } + + func tapActionAtPoint(_ point: CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction { + if let currentLayout = self.currentLayout { + for item in currentLayout.items { + let frame = self.effectiveFrameForItem(item) + if frame.contains(point) { + if item is InstantPagePeerReferenceItem { + return .fail + } else if item is InstantPageAudioItem { + return .fail + } else if item is InstantPageArticleItem { + return .fail + } else if item is InstantPageFeedbackItem { + return .fail + } else if let item = item as? InstantPageDetailsItem { + for (_, itemNode) in self.visibleItemsWithNodes { + if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item { + return itemNode.tapActionAtPoint(point.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY)) + } + } + } + if !(item is InstantPageImageItem || item is InstantPagePlayableVideoItem) { + break + } + } + } + } + return .waitForSingleTap + } + + private func updateTouchesAtPoint(_ location: CGPoint?) { + var rects: [CGRect]? + if let location = location, let currentLayout = self.currentLayout { + for item in currentLayout.items { + let itemFrame = self.effectiveFrameForItem(item) + if itemFrame.contains(location) { + var contentOffset = CGPoint() + if let item = item as? InstantPageScrollableItem { + contentOffset = self.scrollableContentOffset(item: item) + } + var itemRects = item.linkSelectionRects(at: location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY)) + + for i in 0 ..< itemRects.count { + itemRects[i] = itemRects[i].offsetBy(dx: itemFrame.minX - contentOffset.x, dy: itemFrame.minY).insetBy(dx: -2.0, dy: -2.0) + } + if !itemRects.isEmpty { + rects = itemRects + break + } + } + } + } + + if let rects = rects { + let linkHighlightingNode: LinkHighlightingNode + if let current = self.linkHighlightingNode { + linkHighlightingNode = current + } else { + let highlightColor = self.theme.linkHighlightColor + linkHighlightingNode = LinkHighlightingNode(color: highlightColor) + linkHighlightingNode.isUserInteractionEnabled = false + self.linkHighlightingNode = linkHighlightingNode + self.scrollNode.addSubnode(linkHighlightingNode) + } + linkHighlightingNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size) + linkHighlightingNode.updateRects(rects) + } else if let linkHighlightingNode = self.linkHighlightingNode { + self.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + } + + private func updateWebPage(_ webPage: TelegramMediaWebpage?, anchor: String?, state: InstantPageStoredState? = nil) { + if self.webPage != webPage { + if self.webPage != nil && self.currentLayout != nil { + if let snaphotView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) { + self.scrollNode.view.superview?.insertSubview(snaphotView, aboveSubview: self.scrollNode.view) + snaphotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snaphotView] _ in + snaphotView?.removeFromSuperview() + }) + } + } + + self.setupScrollOffsetOnLayout = self.webPage == nil + self.webPage = webPage + if let anchor = anchor { + self.initialAnchor = anchor.removingPercentEncoding + } else if let state = state { + self.initialState = state + if !state.details.isEmpty { + var storedExpandedDetails: [Int: Bool] = [:] + for state in state.details { + storedExpandedDetails[Int(clamping: state.index)] = state.expanded + } + self.currentExpandedDetails = storedExpandedDetails + } + } + self.currentLayout = nil + self.updatePageLayout() + + self.scrollNode.frame = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0) + self.requestLayout(transition: .immediate) + + if let webPage = webPage, case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, instantPage.isComplete { + self.loadProgress.set(1.0) + + if let anchor = self.pendingAnchor { + self.pendingAnchor = nil + self.scrollToAnchor(anchor) + } + } + } + } + + private func requestLayout(transition: ContainedViewLayoutTransition) { + guard let (size, insets, fullInsets) = self.containerLayout else { + return + } + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: .zero, transition: transition) + } + + func reload() { + } + + func stop() { + } + + func navigateBack() { + + } + + func navigateForward() { + + } + + func navigateTo(historyItem: BrowserContentState.HistoryItem) { + + } + + var currentFontState = BrowserPresentationState.FontState(size: 100, isSerif: false) + func updateFontState(_ state: BrowserPresentationState.FontState) { + self.currentFontState = state + + let fontSize: InstantPagePresentationFontSize + switch state.size { + case 50: + fontSize = .xxsmall + case 75: + fontSize = .xsmall + case 85: + fontSize = .small + case 100: + fontSize = .standard + case 115: + fontSize = .large + case 125: + fontSize = .xlarge + case 150: + fontSize = .xxlarge + default: + fontSize = .standard + } + + self.settings = InstantPagePresentationSettings( + themeType: self.presentationData.theme.overallDarkAppearance ? .dark : .light, + fontSize: fontSize, + forceSerif: state.isSerif, + autoNightMode: false, + ignoreAutoNightModeUntil: 0 + ) + self.theme = instantPageThemeForType(self.presentationData.theme.overallDarkAppearance ? .dark : .light, settings: self.settings) + self.updatePageLayout() + self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) + } + + func setSearch(_ query: String?, completion: ((Int) -> Void)?) { + + } + + func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) { + + } + + func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) { + + } + + func scrollToTop() { + let scrollView = self.scrollNode.view + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true) + } + + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: safeInsets, transition: transition.containedViewLayoutTransition) + } + + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { + self.containerLayout = (size, insets, fullInsets) + + var updateVisibleItems = false + let resetContentOffset = self.scrollNode.bounds.size.width.isZero || self.setupScrollOffsetOnLayout || !(self.initialAnchor ?? "").isEmpty + + var scrollInsets = insets + scrollInsets.top = 0.0 + if self.scrollNode.view.contentInset != insets { + self.scrollNode.view.contentInset = scrollInsets + self.scrollNode.view.scrollIndicatorInsets = scrollInsets + } + + self.wrapperNode.frame = CGRect(origin: .zero, size: size) + + let scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top)) + let scrollFrameUpdated = self.scrollNode.bounds.size != scrollFrame.size + if scrollFrameUpdated { + let widthUpdated = self.scrollNode.bounds.size.width != scrollFrame.width + self.scrollNode.frame = scrollFrame + if widthUpdated { + self.updatePageLayout() + } + updateVisibleItems = true + } + + if resetContentOffset { + var didSetScrollOffset = false + var contentOffset = CGPoint(x: 0.0, y: -self.scrollNode.view.contentInset.top) + if let state = self.initialState { + didSetScrollOffset = true + contentOffset = CGPoint(x: 0.0, y: CGFloat(state.contentOffset)) + } else if let anchor = self.initialAnchor, !anchor.isEmpty { + self.initialAnchor = nil + if let items = self.currentLayout?.items { + didSetScrollOffset = true + if let (item, lineOffset, _, _) = self.findAnchorItem(anchor, items: items) { + contentOffset = CGPoint(x: 0.0, y: item.frame.minY + lineOffset - self.scrollNode.view.contentInset.top) + } + } + } else { + didSetScrollOffset = true + } + self.scrollNode.view.contentOffset = contentOffset + if didSetScrollOffset { + //update scroll event + if self.currentLayout != nil { + self.setupScrollOffsetOnLayout = false + } + } + } + + if updateVisibleItems { + self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) + } + } + + private func updatePageLayout() { + guard let (size, insets, _) = self.containerLayout, let webPage = self.webPage else { + return + } + + let currentLayout = instantPageLayoutForWebPage(webPage, userLocation: self.sourceLocation.userLocation, boundingWidth: size.width, safeInset: insets.left, strings: self.presentationData.strings, theme: self.theme, dateTimeFormat: self.presentationData.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights) + + for (_, tileNode) in self.visibleTiles { + tileNode.removeFromSupernode() + } + self.visibleTiles.removeAll() + + let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: size.width) + + var currentDetailsItems: [InstantPageDetailsItem] = [] + var currentLayoutItemsWithNodes: [InstantPageItem] = [] + var distanceThresholdGroupCount: [Int : Int] = [:] + + var expandedDetails: [Int : Bool] = [:] + + var detailsIndex = -1 + for item in currentLayout.items { + if item.wantsNode { + currentLayoutItemsWithNodes.append(item) + if let group = item.distanceThresholdGroup() { + let count: Int + if let currentCount = distanceThresholdGroupCount[Int(group)] { + count = currentCount + } else { + count = 0 + } + distanceThresholdGroupCount[Int(group)] = count + 1 + } + if let detailsItem = item as? InstantPageDetailsItem { + detailsIndex += 1 + expandedDetails[detailsIndex] = detailsItem.initiallyExpanded + currentDetailsItems.append(detailsItem) + } + } + } + + if var currentExpandedDetails = self.currentExpandedDetails { + for (index, expanded) in expandedDetails { + if currentExpandedDetails[index] == nil { + currentExpandedDetails[index] = expanded + } + } + self.currentExpandedDetails = currentExpandedDetails + } else { + self.currentExpandedDetails = expandedDetails + } + + let accessibilityAreas = instantPageAccessibilityAreasFromLayout(currentLayout, boundingWidth: size.width) + + self.currentLayout = currentLayout + self.currentLayoutTiles = currentLayoutTiles + self.currentLayoutItemsWithNodes = currentLayoutItemsWithNodes + self.currentDetailsItems = currentDetailsItems + self.distanceThresholdGroupCount = distanceThresholdGroupCount + + for areaNode in self.currentAccessibilityAreas { + areaNode.removeFromSupernode() + } + for areaNode in accessibilityAreas { + self.scrollNode.addSubnode(areaNode) + } + self.currentAccessibilityAreas = accessibilityAreas + + self.scrollNode.view.contentSize = currentLayout.contentSize + self.scrollNodeFooter.frame = CGRect(origin: CGPoint(x: 0.0, y: currentLayout.contentSize.height), size: CGSize(width: size.width, height: 2000.0)) + } + + func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) { + var visibleTileIndices = Set() + var visibleItemIndices = Set() + + var topNode: ASDisplayNode? + let topTileNode = topNode + if let scrollSubnodes = self.scrollNode.subnodes { + for node in scrollSubnodes.reversed() { + if let node = node as? InstantPageTileNode { + topNode = node + break + } + } + } + + var collapseOffset: CGFloat = 0.0 + let transition: ContainedViewLayoutTransition + if animated { + transition = .animated(duration: 0.3, curve: .spring) + } else { + transition = .immediate + } + + var itemIndex = -1 + var embedIndex = -1 + var detailsIndex = -1 + + var previousDetailsNode: InstantPageDetailsNode? + + for item in self.currentLayoutItemsWithNodes { + itemIndex += 1 + if item is InstantPageWebEmbedItem { + embedIndex += 1 + } + if let imageItem = item as? InstantPageImageItem, imageItem.media.media._asMedia() is TelegramMediaWebpage { + embedIndex += 1 + } + if item is InstantPageDetailsItem { + detailsIndex += 1 + } + + var itemThreshold: CGFloat = 0.0 + if let group = item.distanceThresholdGroup() { + var count: Int = 0 + if let currentCount = self.distanceThresholdGroupCount[group] { + count = currentCount + } + itemThreshold = item.distanceThresholdWithGroupCount(count) + } + + var itemFrame = item.frame.offsetBy(dx: 0.0, dy: -collapseOffset) + var thresholdedItemFrame = itemFrame + thresholdedItemFrame.origin.y -= itemThreshold + thresholdedItemFrame.size.height += itemThreshold * 2.0 + + if let detailsItem = item as? InstantPageDetailsItem, let expanded = self.currentExpandedDetails?[detailsIndex] { + let height = expanded ? self.effectiveSizeForDetails(detailsItem).height : detailsItem.titleHeight + collapseOffset += itemFrame.height - height + itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: height)) + } + + if visibleBounds.intersects(thresholdedItemFrame) { + visibleItemIndices.insert(itemIndex) + + var itemNode = self.visibleItemsWithNodes[itemIndex] + if let currentItemNode = itemNode { + if !item.matchesNode(currentItemNode) { + currentItemNode.removeFromSupernode() + self.visibleItemsWithNodes.removeValue(forKey: itemIndex) + itemNode = nil + } + } + + if itemNode == nil { + let itemIndex = itemIndex + let embedIndex = embedIndex + let detailsIndex = detailsIndex + if let newNode = item.node(context: self.context, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, theme: self.theme, sourceLocation: self.sourceLocation, openMedia: { [weak self] media in + self?.openMedia(media) + }, longPressMedia: { [weak self] media in + self?.longPressMedia(media) + }, activatePinchPreview: { [weak self] sourceNode in + self?.activatePinchPreview(sourceNode: sourceNode) + }, pinchPreviewFinished: { [weak self] itemNode in + self?.pinchPreviewFinished(itemNode: itemNode) + }, openPeer: { [weak self] peerId in + self?.openPeer(peerId) + }, openUrl: { [weak self] url in + self?.openUrl(url) + }, updateWebEmbedHeight: { [weak self] height in + self?.updateWebEmbedHeight(embedIndex, height) + }, updateDetailsExpanded: { [weak self] expanded in + self?.updateDetailsExpanded(detailsIndex, expanded) + }, currentExpandedDetails: self.currentExpandedDetails) { + newNode.frame = itemFrame + newNode.updateLayout(size: itemFrame.size, transition: transition) + if let topNode = topNode { + self.scrollNode.insertSubnode(newNode, aboveSubnode: topNode) + } else { + self.scrollNode.insertSubnode(newNode, at: 0) + } + topNode = newNode + self.visibleItemsWithNodes[itemIndex] = newNode + itemNode = newNode + + if let itemNode = itemNode as? InstantPageDetailsNode { + itemNode.requestLayoutUpdate = { [weak self] animated in + if let strongSelf = self { + strongSelf.updateVisibleItems(visibleBounds: strongSelf.scrollNode.view.bounds, animated: animated) + } + } + + if let previousDetailsNode = previousDetailsNode { + if itemNode.frame.minY - previousDetailsNode.frame.maxY < 1.0 { + itemNode.previousNode = previousDetailsNode + } + } + previousDetailsNode = itemNode + } + } + } else { + if let itemNode = itemNode, itemNode.frame != itemFrame { + transition.updateFrame(node: itemNode, frame: itemFrame) + itemNode.updateLayout(size: itemFrame.size, transition: transition) + } + } + + if let itemNode = itemNode as? InstantPageDetailsNode { + itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated) + } + } + } + + topNode = topTileNode + + var tileIndex = -1 + for tile in self.currentLayoutTiles { + tileIndex += 1 + + let tileFrame = effectiveFrameForTile(tile) + var tileVisibleFrame = tileFrame + tileVisibleFrame.origin.y -= 400.0 + tileVisibleFrame.size.height += 400.0 * 2.0 + if tileVisibleFrame.intersects(visibleBounds) { + visibleTileIndices.insert(tileIndex) + + if self.visibleTiles[tileIndex] == nil { + let tileNode = InstantPageTileNode(tile: tile, backgroundColor: theme.pageBackgroundColor) + tileNode.frame = tileFrame + if let topNode = topNode { + self.scrollNode.insertSubnode(tileNode, aboveSubnode: topNode) + } else { + self.scrollNode.insertSubnode(tileNode, at: 0) + } + topNode = tileNode + self.visibleTiles[tileIndex] = tileNode + } else { + if visibleTiles[tileIndex]!.frame != tileFrame { + transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame) + } + } + } + } + + if let currentLayout = self.currentLayout { + let effectiveContentHeight = currentLayout.contentSize.height - collapseOffset + if effectiveContentHeight != self.scrollNode.view.contentSize.height { + transition.animateView { + self.scrollNode.view.contentSize = CGSize(width: currentLayout.contentSize.width, height: effectiveContentHeight) + } + let previousFrame = self.scrollNodeFooter.frame + self.scrollNodeFooter.frame = CGRect(origin: CGPoint(x: 0.0, y: effectiveContentHeight), size: CGSize(width: previousFrame.width, height: 2000.0)) + transition.animateFrame(node: self.scrollNodeFooter, from: previousFrame) + } + } + + var removeTileIndices: [Int] = [] + for (index, tileNode) in self.visibleTiles { + if !visibleTileIndices.contains(index) { + removeTileIndices.append(index) + tileNode.removeFromSupernode() + } + } + for index in removeTileIndices { + self.visibleTiles.removeValue(forKey: index) + } + + var removeItemIndices: [Int] = [] + for (index, itemNode) in self.visibleItemsWithNodes { + if !visibleItemIndices.contains(index) { + removeItemIndices.append(index) + itemNode.removeFromSupernode() + } else { + var itemFrame = itemNode.frame + let itemThreshold: CGFloat = 200.0 + itemFrame.origin.y -= itemThreshold + itemFrame.size.height += itemThreshold * 2.0 + itemNode.updateIsVisible(visibleBounds.intersects(itemFrame)) + } + } + for index in removeItemIndices { + self.visibleItemsWithNodes.removeValue(forKey: index) + } + } + + private struct ScrollingOffsetState: Equatable { + var value: CGFloat + var isDraggingOrDecelerating: Bool + } + + private var previousScrollingOffset: ScrollingOffsetState? + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) + 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) + } + + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + self.snapScrollingOffsetToInsets() + } + } + + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.snapScrollingOffsetToInsets() + } + + private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) { + let scrollView = self.scrollNode.view + let isInteracting = scrollView.isDragging || scrollView.isDecelerating + if let previousScrollingOffsetValue = self.previousScrollingOffset { + let currentBounds = scrollView.bounds + let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) + let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) + + let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue.value + self.onScrollingUpdate(ContentScrollingUpdate( + relativeOffset: relativeOffset, + absoluteOffsetToTopEdge: offsetToTopEdge, + absoluteOffsetToBottomEdge: offsetToBottomEdge, + isReset: isReset, + isInteracting: isInteracting, + transition: transition + )) + } + self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting) + + var readingProgress: CGFloat = 0.0 + if !scrollView.contentSize.height.isZero { + let value = (scrollView.contentOffset.y + scrollView.contentInset.top) / (scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.top) + readingProgress = max(0.0, min(1.0, value)) + } + self.readingProgress.set(readingProgress) + } + + func resetScrolling() { + self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4)) + } + + private func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint { + var contentOffset = CGPoint() + for (_, itemNode) in self.visibleItemsWithNodes { + if let itemNode = itemNode as? InstantPageScrollableNode, itemNode.item === item { + contentOffset = itemNode.contentOffset + break + } + } + return contentOffset + } + + private func nodeForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsNode? { + for (_, itemNode) in self.visibleItemsWithNodes { + if let detailsNode = itemNode as? InstantPageDetailsNode, detailsNode.item === item { + return detailsNode + } + } + return nil + } + + private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize { + if let node = nodeForDetailsItem(item) { + return CGSize(width: item.frame.width, height: node.effectiveContentSize.height + item.titleHeight) + } else { + return item.frame.size + } + } + + private func effectiveFrameForTile(_ tile: InstantPageTile) -> CGRect { + let layoutOrigin = tile.frame.origin + var origin = layoutOrigin + for item in self.currentDetailsItems { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + if layoutOrigin.y >= item.frame.maxY { + let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight + origin.y += height - item.frame.height + } + } + return CGRect(origin: origin, size: tile.frame.size) + } + + private func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect { + let layoutOrigin = item.frame.origin + var origin = layoutOrigin + + for item in self.currentDetailsItems { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + if layoutOrigin.y >= item.frame.maxY { + let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight + origin.y += height - item.frame.height + } + } + + if let item = item as? InstantPageDetailsItem { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight + return CGRect(origin: origin, size: CGSize(width: item.frame.width, height: height)) + } else { + return CGRect(origin: origin, size: item.frame.size) + } + } + + private func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? { + if let currentLayout = self.currentLayout { + for item in currentLayout.items { + let itemFrame = self.effectiveFrameForItem(item) + if itemFrame.contains(location) { + if let item = item as? InstantPageTextItem, item.selectable { + return (item, CGPoint(x: itemFrame.minX - item.frame.minX, y: itemFrame.minY - item.frame.minY)) + } else if let item = item as? InstantPageScrollableItem { + let contentOffset = scrollableContentOffset(item: item) + if let (textItem, parentOffset) = item.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY)) { + return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x - contentOffset.x, dy: parentOffset.y)) + } + } else if let item = item as? InstantPageDetailsItem { + for (_, itemNode) in self.visibleItemsWithNodes { + if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item { + if let (textItem, parentOffset) = itemNode.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX, dy: -itemFrame.minY)) { + return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x, dy: parentOffset.y)) + } + } + } + } + } + } + } + return nil + } + + private func urlForTapLocation(_ location: CGPoint) -> InstantPageUrlItem? { + if let (item, parentOffset) = self.textItemAtLocation(location) { + return item.urlAttribute(at: location.offsetBy(dx: -item.frame.minX - parentOffset.x, dy: -item.frame.minY - parentOffset.y)) + } + return nil + } + + private func openUrl(_ url: InstantPageUrlItem) { + var baseUrl = url.url + var anchor: String? + if let anchorRange = url.url.range(of: "#") { + anchor = String(baseUrl[anchorRange.upperBound...]).removingPercentEncoding + baseUrl = String(baseUrl[.. deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + strongSelf.loadProgress.set(0.07) + switch result { + case let .externalUrl(externalUrl): + if let webpageId = url.webpageId { + var anchor: String? + if let anchorRange = externalUrl.range(of: "#") { + anchor = String(externalUrl[anchorRange.upperBound...]) + } + strongSelf.loadWebpageDisposable.set((webpagePreviewWithProgress(account: strongSelf.context.account, urls: [externalUrl], webpageId: webpageId) + |> deliverOnMainQueue).start(next: { result in + if let strongSelf = self { + switch result { + case let .result(webpageResult): + if let webpageResult = webpageResult, case .Loaded = webpageResult.webpage.content { + strongSelf.loadProgress.set(1.0) + strongSelf.pushContent(.instantPage(webPage: webpageResult.webpage, anchor: anchor, sourceLocation: strongSelf.sourceLocation)) + } + break + case let .progress(progress): + strongSelf.loadProgress.set(CGFloat(0.07 + progress * (1.0 - 0.07))) + } + } + })) + } else { + strongSelf.loadProgress.set(1.0) + strongSelf.pushContent(.webPage(url: externalUrl)) + } + case let .instantView(webpage, anchor): + strongSelf.loadProgress.set(1.0) + strongSelf.pushContent(.instantPage(webPage: webpage, anchor: anchor, sourceLocation: strongSelf.sourceLocation)) + default: + strongSelf.loadProgress.set(1.0) + strongSelf.minimize() + strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.getNavigationController(), forceExternal: false, openPeer: { peer, navigation in + switch navigation { + case let .chat(_, subject, peekData): + if let navigationController = strongSelf.getNavigationController() { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: subject, peekData: peekData)) + } + case let .withBotStartPayload(botStart): + if let navigationController = strongSelf.getNavigationController() { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), botStart: botStart, keepStack: .always)) + } + case let .withAttachBot(attachBotStart): + if let navigationController = strongSelf.getNavigationController() { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), attachBotStart: attachBotStart)) + } + case let .withBotApp(botAppStart): + if let navigationController = strongSelf.getNavigationController() { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), botAppStart: botAppStart)) + } + case .info: + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)) + |> deliverOnMainQueue).start(next: { peer in + if let strongSelf = self, let peer = peer { + if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + strongSelf.getNavigationController()?.pushViewController(controller) + } + } + }) + default: + break + } + }, + sendFile: nil, + sendSticker: nil, + sendEmoji: nil, + requestMessageActionUrlAuth: nil, + joinVoiceChat: nil, + present: { c, a in + self?.present(c, a) + }, dismissInput: { [weak self] in + self?.endEditing(true) + }, contentContext: nil, progress: nil, completion: nil) + } + } + })) + } + + private func openUrlIn(_ url: InstantPageUrlItem) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = OpenInActionSheetController(context: self.context, item: .url(url: url.url), openUrl: { [weak self] url in + if let self { + self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + } + }) + self.present(actionSheet, nil) + } + + private func openMedia(_ media: InstantPageMedia) { + guard let items = self.currentLayout?.items, let webPage = self.webPage else { + return + } + + func mediasFromItems(_ items: [InstantPageItem]) -> [InstantPageMedia] { + var medias: [InstantPageMedia] = [] + for item in items { + if let detailsItem = item as? InstantPageDetailsItem { + medias.append(contentsOf: mediasFromItems(detailsItem.items)) + } else { + if let item = item as? InstantPageImageItem, item.interactive { + medias.append(contentsOf: item.medias) + } else if let item = item as? InstantPagePlayableVideoItem, item.interactive { + medias.append(contentsOf: item.medias) + } + } + } + return medias + } + + if case let .geo(map) = media.media { + let controllerParams = LocationViewParams(sendLiveLocation: { _ in + }, stopLiveLocation: { _ in + }, openUrl: { _ in }, openPeer: { _ in + }, showAll: false) + + let peer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", 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 message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peer, text: "", attributes: [], media: [map], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + + let controller = LocationViewController(context: self.context, subject: EngineMessage(message), params: controllerParams) + self.push(controller) + return + } + + if case let .file(file) = media.media, (file.isVoice || file.isMusic) { + var medias: [InstantPageMedia] = [] + var initialIndex = 0 + for item in items { + for itemMedia in item.medias { + if case let .file(itemFile) = itemMedia.media, (itemFile.isVoice || itemFile.isMusic) { + if itemMedia.index == media.index { + initialIndex = medias.count + } + medias.append(itemMedia) + } + } + } + self.context.sharedContext.mediaManager.setPlaylist((self.context.account, InstantPageMediaPlaylist(webPage: webPage, items: medias, initialItemIndex: initialIndex)), type: file.isVoice ? .voice : .music, control: .playback(.play)) + return + } + + var fromPlayingVideo = false + + var entries: [InstantPageGalleryEntry] = [] + if case let .webpage(webPage) = media.media { + entries.append(InstantPageGalleryEntry(index: 0, pageId: webPage.webpageId, media: media, caption: nil, credit: nil, location: nil)) + } else if case let .file(file) = media.media, file.isAnimated { + fromPlayingVideo = true + entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: nil)) + } else { + fromPlayingVideo = true + var medias: [InstantPageMedia] = mediasFromItems(items) + medias = medias.filter { item in + switch item.media { + case .image, .file: + return true + default: + return false + } + } + + for media in medias { + entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: InstantPageGalleryEntryLocation(position: Int32(entries.count), totalCount: Int32(medias.count)))) + } + } + + var centralIndex: Int? + for i in 0 ..< entries.count { + if entries[i].media == media { + centralIndex = i + break + } + } + + if let centralIndex = centralIndex { + let controller = InstantPageGalleryController(context: self.context, userLocation: self.sourceLocation.userLocation, webPage: webPage, entries: entries, centralIndex: centralIndex, fromPlayingVideo: fromPlayingVideo, replaceRootController: { _, _ in + }, baseNavigationController: self.getNavigationController()) + self.hiddenMediaDisposable.set((controller.hiddenMedia |> deliverOnMainQueue).start(next: { [weak self] entry in + if let strongSelf = self { + for (_, itemNode) in strongSelf.visibleItemsWithNodes { + itemNode.updateHiddenMedia(media: entry?.media) + } + } + })) + controller.openUrl = { [weak self] url in + self?.openUrl(url) + } + self.present(controller, InstantPageGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry -> GalleryTransitionArguments? in + if let strongSelf = self { + for (_, itemNode) in strongSelf.visibleItemsWithNodes { + if let transitionNode = itemNode.transitionNode(media: entry.media) { + return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { view in + if let strongSelf = self { + strongSelf.scrollNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.scrollNode.view) + } + }) + } + } + } + return nil + })) + } + } + + private func longPressMedia(_ media: InstantPageMedia) { + let controller = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in + if let self, let image = media.media._asMedia() as? TelegramMediaImage { + let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + let _ = copyToPasteboard(context: self.context, postbox: self.context.account.postbox, userLocation: self.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() + } + }), ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_LinkDialogSave, accessibilityLabel: self.presentationData.strings.Conversation_LinkDialogSave), action: { [weak self] in + if let self, let image = media.media._asMedia() as? TelegramMediaImage { + let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + let _ = saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: self.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() + } + }), ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuShare, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuShare), action: { [weak self] in + if let self, let webPage = self.webPage, let image = media.media._asMedia() as? TelegramMediaImage { + self.present(ShareController(context: self.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil) + } + })], catchTapsOutside: true) + self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + if let self { + for (_, itemNode) in self.visibleItemsWithNodes { + if let (node, _, _) = itemNode.transitionNode(media: media) { + return (self.scrollNode, node.convert(node.bounds, to: self.scrollNode), self.wrapperNode, self.wrapperNode.bounds) + } + } + } + return nil + })) + } + + private func activatePinchPreview(sourceNode: PinchSourceContainerNode) { + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { [weak self] in + guard let self else { + return CGRect() + } + let localRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.width, height: self.bounds.height)) + return self.convert(localRect, to: nil) + }) + self.presentInGlobalOverlay(pinchController) + } + + private func pinchPreviewFinished(itemNode: ASDisplayNode) { + for (_, listItemNode) in self.visibleItemsWithNodes { + if let listItemNode = listItemNode as? InstantPagePeerReferenceNode { + if listItemNode.frame.intersects(itemNode.frame) && listItemNode.frame.maxY <= itemNode.frame.maxY + 2.0 { + listItemNode.layer.animateAlpha(from: 0.0, to: listItemNode.alpha, duration: 0.25) + break + } + } + } + } + + @objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + if let url = self.urlForTapLocation(location) { + self.openUrl(url) + } + case .longTap: + if let url = self.urlForTapLocation(location) { + let canOpenIn = availableOpenInOptions(context: self.context, item: .url(url: url.url)).count > 1 + let openText = canOpenIn ? self.presentationData.strings.Conversation_FileOpenIn : self.presentationData.strings.Conversation_LinkDialogOpen + let actionSheet = ActionSheetController(instantPageTheme: self.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: url.url), + ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + if canOpenIn { + strongSelf.openUrlIn(url) + } else { + strongSelf.openUrl(url) + } + } + }), + ActionSheetButtonItem(title: self.presentationData.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = url.url + }), + ActionSheetButtonItem(title: self.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let link = URL(string: url.url) { + let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + } + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + self.present(actionSheet, nil) + } else if let (item, parentOffset) = self.textItemAtLocation(location) { + let textFrame = item.frame + var itemRects = item.lineRects() + for i in 0 ..< itemRects.count { + itemRects[i] = itemRects[i].offsetBy(dx: parentOffset.x + textFrame.minX, dy: parentOffset.y + textFrame.minY).insetBy(dx: -2.0, dy: -2.0) + } + self.updateTextSelectionRects(itemRects, text: item.plainText()) + } + default: + break + } + } + default: + break + } + } + + private func updateTextSelectionRects(_ rects: [CGRect], text: String?) { + if let text = text, !rects.isEmpty { + let textSelectionNode: LinkHighlightingNode + if let current = self.textSelectionNode { + textSelectionNode = current + } else { + textSelectionNode = LinkHighlightingNode(color: UIColor.lightGray.withAlphaComponent(0.4)) + textSelectionNode.isUserInteractionEnabled = false + self.textSelectionNode = textSelectionNode + self.scrollNode.addSubnode(textSelectionNode) + } + textSelectionNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size) + textSelectionNode.updateRects(rects) + + var coveringRect = rects[0] + for i in 1 ..< rects.count { + coveringRect = coveringRect.union(rects[i]) + } + + let context = self.context + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = self.presentationData.strings + let _ = (context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] sharedData in + let translationSettings: TranslationSettings + if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) { + translationSettings = current + } else { + translationSettings = TranslationSettings.defaultSettings + } + + var actions: [ContextMenuAction] = [ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuCopy, accessibilityLabel: strings.Conversation_ContextMenuCopy), action: { [weak self] in + UIPasteboard.general.string = text + + if let strongSelf = self { + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + } + }), ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuShare, accessibilityLabel: strings.Conversation_ContextMenuShare), action: { [weak self] in + if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { + strongSelf.present(ShareController(context: strongSelf.context, subject: .quote(text: text, url: content.url)), nil) + } + })] + + let (canTranslate, language) = canTranslateText(context: context, text: text, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: false, ignoredLanguages: translationSettings.ignoredLanguages) + if canTranslate { + actions.append(ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuTranslate, accessibilityLabel: strings.Conversation_ContextMenuTranslate), action: { [weak self] in + let controller = TranslateScreen(context: context, text: text, canCopy: true, fromLanguage: language) + controller.pushController = { [weak self] c in + self?.getNavigationController()?._keepModalDismissProgress = true + self?.push(c) + } + controller.presentController = { [weak self] c in + self?.present(c, nil) + } + self?.present(controller, nil) + })) + } + + let controller = makeContextMenuController(actions: actions) + controller.dismissed = { [weak self] in + self?.updateTextSelectionRects([], text: nil) + } + self?.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + if let strongSelf = self { + return (strongSelf.scrollNode, coveringRect.insetBy(dx: -3.0, dy: -3.0), strongSelf.wrapperNode, strongSelf.wrapperNode.bounds) + } else { + return nil + } + })) + }) + + textSelectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + } else if let textSelectionNode = self.textSelectionNode { + self.textSelectionNode = nil + textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in + textSelectionNode?.removeFromSupernode() + }) + } + } + + private func findAnchorItem(_ anchor: String, items: [InstantPageItem]) -> (InstantPageItem, CGFloat, Bool, [InstantPageDetailsItem])? { + for item in items { + if let item = item as? InstantPageAnchorItem, item.anchor == anchor { + return (item, -10.0, false, []) + } else if let item = item as? InstantPageTextItem { + if let (lineIndex, empty) = item.anchors[anchor] { + return (item, item.lines[lineIndex].frame.minY - 10.0, !empty, []) + } + } + else if let item = item as? InstantPageTableItem { + if let (offset, empty) = item.anchors[anchor] { + return (item, offset - 10.0, !empty, []) + } + } + else if let item = item as? InstantPageDetailsItem { + if let (foundItem, offset, reference, detailsItems) = self.findAnchorItem(anchor, items: item.items) { + var detailsItems = detailsItems + detailsItems.insert(item, at: 0) + return (foundItem, offset, reference, detailsItems) + } + } + } + return nil + } + + private func presentReferenceView(item: InstantPageTextItem, referenceAnchor: String) { + guard let webPage = self.webPage else { + return + } + + var targetAnchor: InstantPageTextAnchorItem? + for (name, (line, _)) in item.anchors { + if name == referenceAnchor { + let anchors = item.lines[line].anchorItems + for anchor in anchors { + if anchor.name == referenceAnchor { + targetAnchor = anchor + break + } + } + } + } + + guard let anchorText = targetAnchor?.anchorText else { + return + } + + let controller = InstantPageReferenceController(context: self.context, sourceLocation: self.sourceLocation, theme: theme, webPage: webPage, anchorText: anchorText, openUrl: { [weak self] url in + self?.openUrl(url) + }, openUrlIn: { [weak self] url in + self?.openUrlIn(url) + }, present: { [weak self] c, a in + self?.present(c, a) + }) + self.present(controller, nil) + } + + private func scrollToAnchor(_ anchor: String) { + guard let items = self.currentLayout?.items else { + return + } + + if !anchor.isEmpty { + if let (item, lineOffset, reference, detailsItems) = findAnchorItem(String(anchor), items: items) { + if let item = item as? InstantPageTextItem, reference { + self.presentReferenceView(item: item, referenceAnchor: anchor) + } else { + var previousDetailsNode: InstantPageDetailsNode? + var containerOffset: CGFloat = 0.0 + for detailsItem in detailsItems { + if let previousNode = previousDetailsNode { + previousNode.contentNode.updateDetailsExpanded(detailsItem.index, true, animated: false) + let frame = previousNode.effectiveFrameForItem(detailsItem) + containerOffset += frame.minY + + previousDetailsNode = previousNode.contentNode.nodeForDetailsItem(detailsItem) + previousDetailsNode?.setExpanded(true, animated: false) + } else { + self.updateDetailsExpanded(detailsItem.index, true, animated: false) + let frame = self.effectiveFrameForItem(detailsItem) + containerOffset += frame.minY + + previousDetailsNode = self.nodeForDetailsItem(detailsItem) + previousDetailsNode?.setExpanded(true, animated: false) + } + } + + let frame: CGRect + if let previousDetailsNode = previousDetailsNode { + frame = previousDetailsNode.effectiveFrameForItem(item) + } else { + frame = self.effectiveFrameForItem(item) + } + + var targetY = min(containerOffset + frame.minY + lineOffset, self.scrollNode.view.contentSize.height - self.scrollNode.frame.height) + if targetY < self.scrollNode.view.contentOffset.y { + targetY -= self.scrollNode.view.contentInset.top + } else { + targetY -= self.containerLayout?.insets.top ?? 20.0 + } + self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: targetY), animated: true) + } + } else if case let .Loaded(content) = self.webPage?.content, let instantPage = content.instantPage, !instantPage.isComplete { // self.loadProgress.set(0.5) -// self.pendingAnchor = anchor -// } -// } else { -// self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: -self.scrollNode.view.contentInset.top), animated: true) -// } -// } -// -// private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) { -// let currentHeight = self.currentWebEmbedHeights[index] -// if height != currentHeight { -// if let currentHeight = currentHeight, currentHeight > height { -// return -// } -// self.currentWebEmbedHeights[index] = height -// -// let signal: Signal = (.complete() |> delay(0.08, queue: Queue.mainQueue())) -// self.updateLayoutDisposable.set(signal.start(completed: { [weak self] in -// if let strongSelf = self { -// strongSelf.updateLayout() -// strongSelf.updateVisibleItems(visibleBounds: strongSelf.scrollNode.view.bounds) -// } -// })) -// } -// } -// -// private func updateDetailsExpanded(_ index: Int, _ expanded: Bool, animated: Bool = true) { -// if var currentExpandedDetails = self.currentExpandedDetails { -// currentExpandedDetails[index] = expanded -// self.currentExpandedDetails = currentExpandedDetails -// } -// self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds, animated: animated) -// } -// -//} -// -//final class BrowserInstantPageContent: UIView, BrowserContent { -// var onScrollingUpdate: (ContentScrollingUpdate) -> Void -// -// func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentFlow.ComponentTransition) { -// -// } -// -// private var _state: BrowserContentState -// private let statePromise: Promise -// -// private let webPage: TelegramMediaWebpage -// private var initialized = false -// -// var state: Signal { -// return self.statePromise.get() -// } -// -// init(context: AccountContext, webPage: TelegramMediaWebpage, url: String) { -// self.webPage = webPage -// -// let presentationData = context.sharedContext.currentPresentationData.with { $0 } -// -// let title: String -// if case let .Loaded(content) = webPage.content { -// title = content.title ?? "" -// } else { -// title = "" -// } -// -// self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, contentType: .instantPage) -// self.statePromise = Promise(self._state) -// -// super.init() -// -// -// } -// -// required init?(coder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -// -// func navigateBack() { -// -// } -// -// func navigateForward() { -// -// } -// -// func setFontSize(_ fontSize: CGFloat) { -// -// } -// -// func setForceSerif(_ force: Bool) { -// -// } -// -// func setSearch(_ query: String?, completion: ((Int) -> Void)?) { -// -// } -// -// func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) { -// -// } -// -// func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) { -// -// } -// -// func scrollToTop() { -// -// } -// -// func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { -//// let layout = ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: insets.bottom, right: 0.0), safeInsets: UIEdgeInsets(top: 0.0, left: insets.left, bottom: 0.0, right: insets.right), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false) -//// self.instantPageNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition) -//// self.instantPageNode.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height) -//// //transition.updateFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: 0.0, y: 56.0), size: CGSize(width: size.width, height: size.height - 56.0))) -//// -//// if !self.initialized { -//// self.initialized = true -//// self.instantPageNode.updateWebPage(self.webPage, anchor: nil) -//// } -// } -//} + self.pendingAnchor = anchor + } + } else { + self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: -self.scrollNode.view.contentInset.top), animated: true) + } + } + + private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) { + let currentHeight = self.currentWebEmbedHeights[index] + if height != currentHeight { + if let currentHeight = currentHeight, currentHeight > height { + return + } + self.currentWebEmbedHeights[index] = height + + let signal: Signal = (.complete() |> delay(0.08, queue: Queue.mainQueue())) + self.updateLayoutDisposable.set(signal.start(completed: { [weak self] in + if let self { + self.updatePageLayout() + self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) + } + })) + } + } + + private func updateDetailsExpanded(_ index: Int, _ expanded: Bool, animated: Bool = true) { + if var currentExpandedDetails = self.currentExpandedDetails { + currentExpandedDetails[index] = expanded + self.currentExpandedDetails = currentExpandedDetails + } + self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds, animated: animated) + } + + func addToRecentlyVisited() { + if let webPage = self.webPage { + let _ = addRecentlyVisitedLink(engine: self.context.engine, webPage: webPage).startStandalone() + } + } + + func makeContentSnapshotView() -> UIView? { + return nil + } +} diff --git a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift index 7e20575261d..89e0387e3d0 100644 --- a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift @@ -5,7 +5,30 @@ import ComponentFlow import BlurredBackgroundComponent import ContextUI +final class BrowserNavigationBarEnvironment: Equatable { + public let fraction: CGFloat + + public init(fraction: CGFloat) { + self.fraction = fraction + } + + public static func ==(lhs: BrowserNavigationBarEnvironment, rhs: BrowserNavigationBarEnvironment) -> Bool { + if lhs.fraction != rhs.fraction { + return false + } + return true + } +} + final class BrowserNavigationBarComponent: CombinedComponent { + public class ExternalState { + public fileprivate(set) var centerItemFrame: CGRect + + public init() { + self.centerItemFrame = .zero + } + } + let backgroundColor: UIColor let separatorColor: UIColor let textColor: UIColor @@ -14,12 +37,15 @@ final class BrowserNavigationBarComponent: CombinedComponent { let topInset: CGFloat let height: CGFloat let sideInset: CGFloat + let metrics: LayoutMetrics + let externalState: ExternalState? let leftItems: [AnyComponentWithIdentity] let rightItems: [AnyComponentWithIdentity] - let centerItem: AnyComponentWithIdentity? + let centerItem: AnyComponentWithIdentity? let readingProgress: CGFloat let loadingProgress: Double? let collapseFraction: CGFloat + let activate: () -> Void init( backgroundColor: UIColor, @@ -30,12 +56,15 @@ final class BrowserNavigationBarComponent: CombinedComponent { topInset: CGFloat, height: CGFloat, sideInset: CGFloat, + metrics: LayoutMetrics, + externalState: ExternalState?, leftItems: [AnyComponentWithIdentity], rightItems: [AnyComponentWithIdentity], - centerItem: AnyComponentWithIdentity?, + centerItem: AnyComponentWithIdentity?, readingProgress: CGFloat, loadingProgress: Double?, - collapseFraction: CGFloat + collapseFraction: CGFloat, + activate: @escaping () -> Void ) { self.backgroundColor = backgroundColor self.separatorColor = separatorColor @@ -45,12 +74,15 @@ final class BrowserNavigationBarComponent: CombinedComponent { self.topInset = topInset self.height = height self.sideInset = sideInset + self.metrics = metrics + self.externalState = externalState self.leftItems = leftItems self.rightItems = rightItems self.centerItem = centerItem self.readingProgress = readingProgress self.loadingProgress = loadingProgress self.collapseFraction = collapseFraction + self.activate = activate } static func ==(lhs: BrowserNavigationBarComponent, rhs: BrowserNavigationBarComponent) -> Bool { @@ -78,6 +110,9 @@ final class BrowserNavigationBarComponent: CombinedComponent { if lhs.sideInset != rhs.sideInset { return false } + if lhs.metrics != rhs.metrics { + return false + } if lhs.leftItems != rhs.leftItems { return false } @@ -100,25 +135,28 @@ final class BrowserNavigationBarComponent: CombinedComponent { } static var body: Body { - let background = Child(BlurredBackgroundComponent.self) + let background = Child(Rectangle.self) let readingProgress = Child(Rectangle.self) let separator = Child(Rectangle.self) let loadingProgress = Child(LoadingProgressComponent.self) let leftItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) let rightItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) - let centerItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let centerItems = ChildMap(environment: BrowserNavigationBarEnvironment.self, keyedBy: AnyHashable.self) + let activate = Child(Button.self) return { context in var availableWidth = context.availableSize.width - let sideInset: CGFloat = 11.0 + context.component.sideInset + let sideInset: CGFloat = (context.component.metrics.isTablet ? 20.0 : 16.0) + context.component.sideInset let collapsedHeight: CGFloat = 24.0 let expandedHeight = context.component.height let contentHeight: CGFloat = expandedHeight * (1.0 - context.component.collapseFraction) + collapsedHeight * context.component.collapseFraction let size = CGSize(width: context.availableSize.width, height: context.component.topInset + contentHeight) + let verticalOffset: CGFloat = context.component.metrics.isTablet ? -2.0 : 0.0 + let itemSpacing: CGFloat = context.component.metrics.isTablet ? 26.0 : 8.0 let background = background.update( - component: BlurredBackgroundComponent(color: context.component.backgroundColor), + component: Rectangle(color: context.component.backgroundColor.withAlphaComponent(1.0)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition ) @@ -145,7 +183,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { availableSize: CGSize(width: size.width, height: size.height), transition: context.transition ) - + var leftItemList: [_UpdatedChildComponent] = [] for item in context.component.leftItems { let item = leftItems[item.id].update( @@ -167,28 +205,18 @@ final class BrowserNavigationBarComponent: CombinedComponent { rightItemList.append(item) availableWidth -= item.size.width } - - if !leftItemList.isEmpty || !rightItemList.isEmpty { - availableWidth -= 32.0 - } - - let centerItem = context.component.centerItem.flatMap { item in - centerItems[item.id].update( - component: item.component, - availableSize: CGSize(width: availableWidth, height: expandedHeight), - transition: context.transition - ) - } - if let centerItem = centerItem { - availableWidth -= centerItem.size.width - } - + context.add(background .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) ) + var readingProgressAlpha = context.component.collapseFraction + if leftItemList.isEmpty && rightItemList.isEmpty { + readingProgressAlpha = 0.0 + } context.add(readingProgress .position(CGPoint(x: readingProgress.size.width / 2.0, y: size.height / 2.0)) + .opacity(readingProgressAlpha) ) context.add(separator @@ -203,37 +231,86 @@ final class BrowserNavigationBarComponent: CombinedComponent { var leftItemX = sideInset for item in leftItemList { context.add(item - .position(CGPoint(x: leftItemX + item.size.width / 2.0 - (item.size.width / 2.0 * 0.35 * context.component.collapseFraction), y: context.component.topInset + contentHeight / 2.0)) + .position(CGPoint(x: leftItemX + item.size.width / 2.0 - (item.size.width / 2.0 * 0.35 * context.component.collapseFraction), y: context.component.topInset + contentHeight / 2.0 + verticalOffset)) .scale(1.0 - 0.35 * context.component.collapseFraction) - .appear(.default(scale: false, alpha: true)) - .disappear(.default(scale: false, alpha: true)) + .opacity(1.0 - context.component.collapseFraction) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) ) - leftItemX -= item.size.width + 8.0 - centerLeftInset += item.size.width + 8.0 + leftItemX += item.size.width + itemSpacing + centerLeftInset += item.size.width + itemSpacing } - var centerRightInset = sideInset - var rightItemX = context.availableSize.width - sideInset + var centerRightInset = sideInset - 5.0 + var rightItemX = context.availableSize.width - (sideInset - 5.0) for item in rightItemList.reversed() { context.add(item - .position(CGPoint(x: rightItemX - item.size.width / 2.0 + (item.size.width / 2.0 * 0.35 * context.component.collapseFraction), y: context.component.topInset + contentHeight / 2.0)) + .position(CGPoint(x: rightItemX - item.size.width / 2.0 + (item.size.width / 2.0 * 0.35 * context.component.collapseFraction), y: context.component.topInset + contentHeight / 2.0 + verticalOffset)) .scale(1.0 - 0.35 * context.component.collapseFraction) .opacity(1.0 - context.component.collapseFraction) - .appear(.default(scale: false, alpha: true)) - .disappear(.default(scale: false, alpha: true)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) ) - rightItemX -= item.size.width + 8.0 - centerRightInset += item.size.width + 8.0 + rightItemX -= item.size.width + itemSpacing + centerRightInset += item.size.width + itemSpacing } let maxCenterInset = max(centerLeftInset, centerRightInset) + + if !leftItemList.isEmpty || !rightItemList.isEmpty { + availableWidth -= itemSpacing * CGFloat(max(0, leftItemList.count - 1)) + itemSpacing * CGFloat(max(0, rightItemList.count - 1)) + 30.0 + } + availableWidth -= context.component.sideInset * 2.0 + + let canCenter = availableWidth > 660.0 + availableWidth = min(660.0, availableWidth) + + let environment = BrowserNavigationBarEnvironment(fraction: context.component.collapseFraction) + + let centerItem = context.component.centerItem.flatMap { item in + centerItems[item.id].update( + component: item.component, + environment: { environment }, + availableSize: CGSize(width: availableWidth, height: expandedHeight), + transition: context.transition + ) + } + + var centerX = maxCenterInset + (context.availableSize.width - maxCenterInset * 2.0) / 2.0 + if "".isEmpty { + if canCenter { + centerX = context.availableSize.width / 2.0 + } else { + centerX = centerLeftInset + (context.availableSize.width - centerLeftInset - centerRightInset) / 2.0 + } + } if let centerItem = centerItem { + let centerItemPosition = CGPoint(x: centerX, y: context.component.topInset + contentHeight / 2.0 + verticalOffset) context.add(centerItem - .position(CGPoint(x: maxCenterInset + (context.availableSize.width - maxCenterInset * 2.0) / 2.0, y: context.component.topInset + contentHeight / 2.0)) + .position(centerItemPosition) .scale(1.0 - 0.35 * context.component.collapseFraction) .appear(.default(scale: false, alpha: true)) .disappear(.default(scale: false, alpha: true)) ) + + context.component.externalState?.centerItemFrame = centerItem.size.centered(around: centerItemPosition) + } + + if context.component.collapseFraction == 1.0 { + let activateAction = context.component.activate + let activate = activate.update( + component: Button( + content: AnyComponent(Rectangle(color: UIColor(rgb: 0x000000, alpha: 0.001))), + action: { + activateAction() + } + ), + availableSize: size, + transition: .immediate + ) + context.add(activate + .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + ) } return size @@ -366,7 +443,7 @@ final class ReferenceButtonComponent: Component { final class View: HighlightTrackingButton, ComponentTaggedView { private let sourceView: ContextControllerSourceView let referenceNode: ContextReferenceContentNode - private let componentView: ComponentView + let componentView: ComponentView private var component: ReferenceButtonComponent? diff --git a/submodules/BrowserUI/Sources/BrowserPdfContent.swift b/submodules/BrowserUI/Sources/BrowserPdfContent.swift new file mode 100644 index 00000000000..cb61825774e --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserPdfContent.swift @@ -0,0 +1,472 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramCore +import Postbox +import SwiftSignalKit +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AccountContext +import WebKit +import AppBundle +import PromptUI +import SafariServices +import ShareController +import UndoUI +import UrlEscaping +import PDFKit + +final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { + private let context: AccountContext + private var presentationData: PresentationData + + private let webView: PDFView + private let scrollView: UIScrollView! + + let uuid: UUID + + private var _state: BrowserContentState + private let statePromise: Promise + + var currentState: BrowserContentState { + return self._state + } + var state: Signal { + return self.statePromise.get() + } + + var pushContent: (BrowserScreen.Subject) -> Void = { _ in } + var openAppUrl: (String) -> Void = { _ in } + var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } + var minimize: () -> Void = { } + var close: () -> Void = { } + var present: (ViewController, Any?) -> Void = { _, _ in } + var presentInGlobalOverlay: (ViewController) -> Void = { _ in } + var getNavigationController: () -> NavigationController? = { return nil } + + private var tempFile: TempBoxFile? + + init(context: AccountContext, presentationData: PresentationData, file: TelegramMediaFile) { + self.context = context + self.uuid = UUID() + self.presentationData = presentationData + + self.webView = PDFView() + self.webView.maxScaleFactor = 4.0; + self.webView.minScaleFactor = self.webView.scaleFactorForSizeToFit + self.webView.autoScales = true + + var scrollView: UIScrollView? + for view in self.webView.subviews { + if let view = view as? UIScrollView { + scrollView = view + } else { + for subview in view.subviews { + if let subview = subview as? UIScrollView { + scrollView = subview + } + } + } + } + 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._state = BrowserContentState(title: title, url: "", estimatedProgress: 0.0, readingProgress: 0.0, contentType: .document) + self.statePromise = Promise(self._state) + + super.init(frame: .zero) + + if #available(iOS 15.0, *) { + self.backgroundColor = presentationData.theme.list.plainBackgroundColor + } + self.addSubview(self.webView) + + Queue.mainQueue().after(1.0) { + scrollView?.delegate = self + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + if #available(iOS 15.0, *) { + self.backgroundColor = presentationData.theme.list.plainBackgroundColor + } + if let (size, insets, fullInsets) = self.validLayout { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: .zero, transition: .immediate) + } + } + + 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 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 +// } +// } + } + + private var currentSearchResult: Int = 0 + private var searchResultsCount: Int = 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) +// }) + } + + 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) +// }) + } + + 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() { + self.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.scrollView.contentInset.top), animated: true) + } + + private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets)? + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) { + 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) + + if refresh { + self.webView.reloadInputViews() + } + +// 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) { + let updated = f(self._state) + self._state = updated + self.statePromise.set(.single(self._state)) + } + + private struct ScrollingOffsetState: Equatable { + var value: CGFloat + var isDraggingOrDecelerating: Bool + } + + 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) + } + + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + self.snapScrollingOffsetToInsets() + } + } + + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.snapScrollingOffsetToInsets() + } + + private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) { + guard let scrollView = self.scrollView else { + return + } + let isInteracting = scrollView.isDragging || scrollView.isDecelerating + if let previousScrollingOffsetValue = self.previousScrollingOffset { + let currentBounds = scrollView.bounds + let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) + let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) + + let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue.value + self.onScrollingUpdate(ContentScrollingUpdate( + relativeOffset: relativeOffset, + absoluteOffsetToTopEdge: offsetToTopEdge, + absoluteOffsetToBottomEdge: offsetToBottomEdge, + isReset: isReset, + isInteracting: isInteracting, + transition: transition + )) + } + self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting) + + var readingProgress: CGFloat = 0.0 + if !scrollView.contentSize.height.isZero { + let value = (scrollView.contentOffset.y + scrollView.contentInset.top) / (scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.top) + readingProgress = max(0.0, min(1.0, value)) + } + self.updateState { + $0.withUpdatedReadingProgress(readingProgress) + } + } + + func resetScrolling() { + 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() { + navigationController._keepModalDismissProgress = true + self.minimize() + let controller = BrowserScreen(context: self.context, subject: subject) + navigationController._keepModalDismissProgress = true + navigationController.pushViewController(controller) + } else { + self.pushContent(subject) + } + } + + private func share(url: String) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let shareController = ShareController(context: self.context, subject: .url(url)) + shareController.actionCompleted = { [weak self] in + self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + } + self.present(shareController, nil) + } + + func addToRecentlyVisited() { + } + + func makeContentSnapshotView() -> UIView? { + return nil + } +} diff --git a/submodules/BrowserUI/Sources/BrowserRecentlyVisited.swift b/submodules/BrowserUI/Sources/BrowserRecentlyVisited.swift new file mode 100644 index 00000000000..fc8bb321f93 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserRecentlyVisited.swift @@ -0,0 +1,88 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit +import TelegramUIPreferences + +private struct RecentlyVisitedLinkItemId { + public let rawValue: MemoryBuffer + + var value: String { + return String(data: self.rawValue.makeData(), encoding: .utf8) ?? "" + } + + init(_ rawValue: MemoryBuffer) { + self.rawValue = rawValue + } + + init?(_ value: String) { + if let data = value.data(using: .utf8) { + self.rawValue = MemoryBuffer(data: data) + } else { + return nil + } + } +} + +public final class RecentVisitedLinkItem: Codable { + private enum CodingKeys: String, CodingKey { + case webPage + } + + public let webPage: TelegramMediaWebpage + + public init(webPage: TelegramMediaWebpage) { + self.webPage = webPage + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let webPageData = try container.decodeIfPresent(Data.self, forKey: .webPage) { + self.webPage = PostboxDecoder(buffer: MemoryBuffer(data: webPageData)).decodeRootObject() as! TelegramMediaWebpage + } else { + fatalError() + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + let encoder = PostboxEncoder() + encoder.encodeRootObject(self.webPage) + let webPageData = encoder.makeData() + try container.encode(webPageData, forKey: .webPage) + } +} + +func addRecentlyVisitedLink(engine: TelegramEngine, webPage: TelegramMediaWebpage) -> Signal { + if let url = webPage.content.url, let itemId = RecentlyVisitedLinkItemId(url) { + return engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.browserRecentlyVisited, id: itemId.rawValue, item: RecentVisitedLinkItem(webPage: webPage), removeTailIfCountExceeds: 10) + } else { + return .complete() + } +} + +func removeRecentlyVisitedLink(engine: TelegramEngine, url: String) -> Signal { + if let itemId = RecentlyVisitedLinkItemId(url) { + return engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.browserRecentlyVisited, id: itemId.rawValue) + } else { + return .complete() + } +} + +func clearRecentlyVisitedLinks(engine: TelegramEngine) -> Signal { + return engine.orderedLists.clear(collectionId: ApplicationSpecificOrderedItemListCollectionId.browserRecentlyVisited) +} + +func recentlyVisitedLinks(engine: TelegramEngine) -> Signal<[TelegramMediaWebpage], NoError> { + return engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.browserRecentlyVisited)) + |> map { items -> [TelegramMediaWebpage] in + var result: [TelegramMediaWebpage] = [] + for item in items { + if let link = item.contents.get(RecentVisitedLinkItem.self) { + result.append(link.webPage) + } + } + return result + } +} diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index c2f9f0dc4d9..70fd9ebdc3e 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import SwiftSignalKit import Display +import TelegramCore import TelegramPresentationData import ComponentFlow import ViewControllerComponent @@ -14,6 +15,10 @@ import TelegramUIPreferences import OpenInExternalAppUI import MultilineTextComponent import MinimizedContainer +import InstantPageUI +import NavigationStackComponent +import LottieComponent +import WebKit private let settingsTag = GenericComponentViewTag() @@ -24,6 +29,7 @@ private final class BrowserScreenComponent: CombinedComponent { let contentState: BrowserContentState? let presentationState: BrowserPresentationState let performAction: ActionSlot + let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void let panelCollapseFraction: CGFloat init( @@ -31,12 +37,14 @@ private final class BrowserScreenComponent: CombinedComponent { contentState: BrowserContentState?, presentationState: BrowserPresentationState, performAction: ActionSlot, + performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void, panelCollapseFraction: CGFloat ) { self.context = context self.contentState = contentState self.presentationState = presentationState self.performAction = performAction + self.performHoldAction = performHoldAction self.panelCollapseFraction = panelCollapseFraction } @@ -66,14 +74,21 @@ private final class BrowserScreenComponent: CombinedComponent { static var body: Body { let navigationBar = Child(BrowserNavigationBarComponent.self) let toolbar = Child(BrowserToolbarComponent.self) + let addressList = Child(BrowserAddressListComponent.self) + + let navigationBarExternalState = BrowserNavigationBarComponent.ExternalState() return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let performAction = context.component.performAction + let performHoldAction = context.component.performHoldAction + + let isTablet = environment.metrics.isTablet + let canOpenIn = !(context.component.contentState?.url.hasPrefix("tonsite") ?? false) - let navigationContent: AnyComponentWithIdentity? - let navigationLeftItems: [AnyComponentWithIdentity] - let navigationRightItems: [AnyComponentWithIdentity] + let navigationContent: AnyComponentWithIdentity? + var navigationLeftItems: [AnyComponentWithIdentity] + var navigationRightItems: [AnyComponentWithIdentity] if context.component.presentationState.isSearching { navigationContent = AnyComponentWithIdentity( id: "search", @@ -88,49 +103,204 @@ private final class BrowserScreenComponent: CombinedComponent { navigationLeftItems = [] navigationRightItems = [] } else { - navigationContent = AnyComponentWithIdentity( - id: "title", - component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: context.component.contentState?.title ?? "", font: Font.bold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor, paragraphAlignment: .center)), horizontalAlignment: .center, maximumNumberOfLines: 1) - ) - ) - navigationLeftItems = [ - AnyComponentWithIdentity( - id: "close", + let contentType = context.component.contentState?.contentType ?? .instantPage + switch contentType { + case .webPage: + navigationContent = AnyComponentWithIdentity( + id: "addressBar", component: AnyComponent( - Button( - content: AnyComponent( - BundleIconComponent( - name: "Instant View/Close", - tintColor: environment.theme.rootController.navigationBar.primaryTextColor - ) - ), - action: { - performAction.invoke(.close) - } + AddressBarContentComponent( + theme: environment.theme, + strings: environment.strings, + metrics: environment.metrics, + url: context.component.contentState?.url ?? "", + isSecure: context.component.contentState?.isSecure ?? false, + isExpanded: context.component.presentationState.addressFocused, + performAction: performAction ) ) ) - ] - navigationRightItems = [ - AnyComponentWithIdentity( - id: "close", + case .instantPage, .document: + let title = context.component.contentState?.title ?? "" + navigationContent = AnyComponentWithIdentity( + id: "titleBar_\(title)", component: AnyComponent( - ReferenceButtonComponent( - content: AnyComponent( - BundleIconComponent( - name: "Instant View/Settings", - tintColor: environment.theme.rootController.navigationBar.primaryTextColor - ) - ), - tag: settingsTag, - action: { - performAction.invoke(.openSettings) - } + TitleBarContentComponent( + theme: environment.theme, + title: title ) ) ) - ] + } + + if context.component.presentationState.addressFocused && !isTablet { + navigationLeftItems = [] + navigationRightItems = [] + } else { + navigationLeftItems = [ + AnyComponentWithIdentity( + id: "close", + component: AnyComponent( + Button( + content: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.WebBrowser_Done, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.accentTextColor, paragraphAlignment: .center)), horizontalAlignment: .left, maximumNumberOfLines: 1) + ), + action: { + performAction.invoke(.close) + } + ) + ) + ) + ] + + if isTablet { + #if DEBUG + navigationLeftItems.append( + AnyComponentWithIdentity( + id: "minimize", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Media Gallery/PictureInPictureButton", + tintColor: environment.theme.rootController.navigationBar.accentTextColor + ) + ), + action: { + performAction.invoke(.close) + } + ) + ) + ) + ) + #endif + + let canGoBack = context.component.contentState?.canGoBack ?? false + let canGoForward = context.component.contentState?.canGoForward ?? false + + navigationLeftItems.append( + AnyComponentWithIdentity( + id: "back", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Back", + tintColor: environment.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(canGoBack ? 1.0 : 0.4) + ) + ), + action: { + performAction.invoke(.navigateBack) + } + ) + ) + ) + ) + + navigationLeftItems.append( + AnyComponentWithIdentity( + id: "forward", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Forward", + tintColor: environment.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(canGoForward ? 1.0 : 0.4) + ) + ), + action: { + performAction.invoke(.navigateForward) + } + ) + ) + ) + ) + } + + navigationRightItems = [ + AnyComponentWithIdentity( + id: "settings", + component: AnyComponent( + ReferenceButtonComponent( + content: AnyComponent( + LottieComponent( + content: LottieComponent.AppBundleContent( + name: "anim_moredots" + ), + color: environment.theme.rootController.navigationBar.accentTextColor, + size: CGSize(width: 30.0, height: 30.0) + ) + ), + tag: settingsTag, + action: { + performAction.invoke(.openSettings) + } + ) + ) + ) + ] + + if isTablet { + navigationRightItems.insert( + AnyComponentWithIdentity( + id: "bookmarks", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Bookmark", + tintColor: environment.theme.rootController.navigationBar.accentTextColor + ) + ), + action: { + performAction.invoke(.openBookmarks) + } + ) + ) + ), + 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) + } + ) + ) + ), + at: 0 + ) + if canOpenIn { + navigationRightItems.append( + AnyComponentWithIdentity( + id: "openIn", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Browser", + tintColor: environment.theme.rootController.navigationBar.accentTextColor + ) + ), + action: { + performAction.invoke(.openIn) + } + ) + ) + ) + ) + } + } + } } let collapseFraction = context.component.presentationState.isSearching ? 0.0 : context.component.panelCollapseFraction @@ -145,12 +315,17 @@ private final class BrowserScreenComponent: CombinedComponent { topInset: environment.statusBarHeight, height: environment.navigationHeight - environment.statusBarHeight, sideInset: environment.safeInsets.left, + metrics: environment.metrics, + externalState: navigationBarExternalState, leftItems: navigationLeftItems, rightItems: navigationRightItems, centerItem: navigationContent, - readingProgress: 0.0, + readingProgress: context.component.contentState?.readingProgress ?? 0.0, loadingProgress: context.component.contentState?.estimatedProgress, - collapseFraction: collapseFraction + collapseFraction: collapseFraction, + activate: { + performAction.invoke(.expand) + } ), availableSize: context.availableSize, transition: context.transition @@ -179,10 +354,13 @@ private final class BrowserScreenComponent: CombinedComponent { id: "navigation", component: AnyComponent( NavigationToolbarContentComponent( + accentColor: environment.theme.rootController.navigationBar.accentTextColor, textColor: environment.theme.rootController.navigationBar.primaryTextColor, canGoBack: context.component.contentState?.canGoBack ?? false, canGoForward: context.component.contentState?.canGoForward ?? false, - performAction: performAction + canOpenIn: canOpenIn, + performAction: performAction, + performHoldAction: performHoldAction ) ) ) @@ -195,22 +373,77 @@ private final class BrowserScreenComponent: CombinedComponent { toolbarBottomInset = environment.safeInsets.bottom } - let toolbar = toolbar.update( - component: BrowserToolbarComponent( - backgroundColor: environment.theme.rootController.navigationBar.blurredBackgroundColor, - separatorColor: environment.theme.rootController.navigationBar.separatorColor, - textColor: environment.theme.rootController.navigationBar.primaryTextColor, - bottomInset: toolbarBottomInset, - sideInset: environment.safeInsets.left, - item: toolbarContent, - collapseFraction: collapseFraction - ), - availableSize: context.availableSize, - transition: context.transition - ) - context.add(toolbar - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0)) - ) + var toolbarSize: CGFloat = 0.0 + if isTablet && !context.component.presentationState.isSearching { + + } else { + let toolbar = toolbar.update( + component: BrowserToolbarComponent( + backgroundColor: environment.theme.rootController.navigationBar.blurredBackgroundColor, + separatorColor: environment.theme.rootController.navigationBar.separatorColor, + textColor: environment.theme.rootController.navigationBar.primaryTextColor, + bottomInset: toolbarBottomInset, + sideInset: environment.safeInsets.left, + item: toolbarContent, + collapseFraction: 0.0 + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(toolbar + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0 + toolbar.size.height * collapseFraction)) + .appear(ComponentTransition.Appear { _, view, transition in + transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: view.frame.height), to: CGPoint(), additive: true) + }) + .disappear(ComponentTransition.Disappear { view, transition, completion in + transition.animatePosition(view: view, from: CGPoint(), to: CGPoint(x: 0.0, y: view.frame.height), additive: true, completion: { _ in + completion() + }) + }) + ) + toolbarSize = toolbar.size.height + } + + if context.component.presentationState.addressFocused { + let addressListSize: CGSize + if isTablet { + addressListSize = context.availableSize + } else { + addressListSize = CGSize(width: context.availableSize.width, height: context.availableSize.height - navigationBar.size.height - toolbarSize) + } + let controller = environment.controller + let addressList = addressList.update( + component: BrowserAddressListComponent( + context: context.component.context, + theme: environment.theme, + strings: environment.strings, + insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), + metrics: environment.metrics, + addressBarFrame: navigationBarExternalState.centerItemFrame, + performAction: performAction, + presentInGlobalOverlay: { c in + controller()?.presentInGlobalOverlay(c) + } + ), + availableSize: addressListSize, + transition: context.transition + ) + + if isTablet { + context.add(addressList + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + } else { + context.add(addressList + .position(CGPoint(x: context.availableSize.width / 2.0, y: navigationBar.size.height + addressList.size.height / 2.0)) + .clipsToBounds(true) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + } + } return context.availableSize } @@ -218,17 +451,23 @@ private final class BrowserScreenComponent: CombinedComponent { } struct BrowserPresentationState: Equatable { - var fontSize: Int32 - var fontIsSerif: Bool + struct FontState: Equatable { + var size: Int32 + var isSerif: Bool + } + var fontState: FontState var isSearching: Bool var searchResultIndex: Int var searchResultCount: Int var searchQueryIsEmpty: Bool + var addressFocused: Bool } -public class BrowserScreen: ViewController { +public class BrowserScreen: ViewController, MinimizableController { enum Action { case close + case reload + case stop case navigateBack case navigateForward case share @@ -243,25 +482,32 @@ public class BrowserScreen: ViewController { case increaseFontSize case resetFontSize case updateFontIsSerif(Bool) + case addBookmark + case openBookmarks + case openAddressBar + case closeAddressBar + case navigateTo(String, Bool) + case expand } - fileprivate final class Node: ViewControllerTracingNode { + final class Node: ViewControllerTracingNode { private weak var controller: BrowserScreen? private let context: AccountContext - private let contentContainerView: UIView - fileprivate var content: BrowserContent? - - private var contentState: BrowserContentState? - private var contentStateDisposable: Disposable? + private let contentContainerView = UIView() + fileprivate let contentNavigationContainer = ComponentView() + private(set) var content: [BrowserContent] = [] + fileprivate var contentState: BrowserContentState? + private var contentStateDisposable = MetaDisposable() private var presentationState: BrowserPresentationState - private let performAction: ActionSlot + private let performAction = ActionSlot() - fileprivate let componentHost: ComponentView + fileprivate let componentHost = ComponentView() private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? private var validLayout: (ContainerViewLayout, CGFloat)? init(controller: BrowserScreen) { @@ -269,52 +515,100 @@ public class BrowserScreen: ViewController { self.controller = controller self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - self.presentationState = BrowserPresentationState(fontSize: 100, fontIsSerif: false, isSearching: false, searchResultIndex: 0, searchResultCount: 0, searchQueryIsEmpty: true) - - self.performAction = ActionSlot() - - self.contentContainerView = UIView() - self.contentContainerView.clipsToBounds = true - - self.componentHost = ComponentView() - + self.presentationState = BrowserPresentationState( + fontState: BrowserPresentationState.FontState(size: 100, isSerif: false), + isSearching: false, + searchResultIndex: 0, + searchResultCount: 0, + searchQueryIsEmpty: true, + addressFocused: false + ) + super.init() - let content: BrowserContent - switch controller.subject { - case let .webPage(url): - content = BrowserWebContent(context: controller.context, url: url) - } - - self.content = content - self.contentStateDisposable = (content.state - |> deliverOnMainQueue).start(next: { [weak self] state in - guard let strongSelf = self else { - return - } - strongSelf.controller?.title = state.title - strongSelf.contentState = state - strongSelf.requestLayout(transition: .immediate) - }).strict() - - self.content?.onScrollingUpdate = { [weak self] update in - self?.onContentScrollingUpdate(update) + self.pushContent(controller.subject, transition: .immediate) + if let content = self.content.last { + content.addToRecentlyVisited() } self.performAction.connect { [weak self] action in - guard let self, let content = self.content, let url = self.contentState?.url else { + guard let self, let content = self.content.last, let url = self.contentState?.url else { return } switch action { case .close: self.controller?.dismiss() + case .reload: + content.reload() + case .stop: + content.stop() case .navigateBack: - content.navigateBack() + if content.currentState.canGoBack { + content.navigateBack() + } else { + self.popContent(transition: .spring(duration: 0.4)) + } case .navigateForward: content.navigateForward() case .share: let presentationData = self.presentationData let shareController = ShareController(context: self.context, subject: .url(url)) + shareController.completed = { [weak self] peerIds in + guard let strongSelf = self else { + return + } + let _ = (strongSelf.context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in + guard let strongSelf = self else { + return + } + + let peers = peerList.compactMap { $0 } + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { + 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 + } 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 + } 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 + } else { + text = "" + } + } + + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.minimize() + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) + }) + } + return false + }), in: .current) + }) + } shareController.actionCompleted = { [weak self] in self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) } @@ -326,7 +620,7 @@ public class BrowserScreen: ViewController { case .openSettings: self.openSettings() case let .updateSearchActive(active): - self.updatePresentationState(animated: true, { state in + self.updatePresentationState(transition: .easeInOut(duration: 0.2), { state in var updatedState = state updatedState.isSearching = active updatedState.searchQueryIsEmpty = true @@ -366,88 +660,323 @@ public class BrowserScreen: ViewController { case .decreaseFontSize: self.updatePresentationState({ state in var updatedState = state - switch state.fontSize { + switch state.fontState.size { case 150: - updatedState.fontSize = 125 + updatedState.fontState.size = 125 case 125: - updatedState.fontSize = 115 + updatedState.fontState.size = 115 case 115: - updatedState.fontSize = 100 + updatedState.fontState.size = 100 case 100: - updatedState.fontSize = 85 + updatedState.fontState.size = 85 case 85: - updatedState.fontSize = 75 + updatedState.fontState.size = 75 case 75: - updatedState.fontSize = 50 + updatedState.fontState.size = 50 default: - updatedState.fontSize = 50 + updatedState.fontState.size = 50 } return updatedState }) - content.setFontSize(CGFloat(self.presentationState.fontSize) / 100.0) + content.updateFontState(self.presentationState.fontState) case .increaseFontSize: self.updatePresentationState({ state in var updatedState = state - switch state.fontSize { + switch state.fontState.size { case 125: - updatedState.fontSize = 150 + updatedState.fontState.size = 150 case 115: - updatedState.fontSize = 125 + updatedState.fontState.size = 125 case 100: - updatedState.fontSize = 115 + updatedState.fontState.size = 115 case 85: - updatedState.fontSize = 100 + updatedState.fontState.size = 100 case 75: - updatedState.fontSize = 85 + updatedState.fontState.size = 85 case 50: - updatedState.fontSize = 75 + updatedState.fontState.size = 75 default: - updatedState.fontSize = 150 + updatedState.fontState.size = 150 } return updatedState }) - content.setFontSize(CGFloat(self.presentationState.fontSize) / 100.0) + content.updateFontState(self.presentationState.fontState) case .resetFontSize: self.updatePresentationState({ state in var updatedState = state - updatedState.fontSize = 100 + updatedState.fontState.size = 100 return updatedState }) - content.setFontSize(CGFloat(self.presentationState.fontSize) / 100.0) + content.updateFontState(self.presentationState.fontState) case let .updateFontIsSerif(value): self.updatePresentationState({ state in var updatedState = state - updatedState.fontIsSerif = value + updatedState.fontState.isSerif = value return updatedState }) - content.setForceSerif(value) + content.updateFontState(self.presentationState.fontState) + case .addBookmark: + if let content = self.content.last { + self.addBookmark(content.currentState.url, showArrow: true) + } + case .openBookmarks: + self.openBookmarks() + case .openAddressBar: + self.updatePresentationState(transition: .spring(duration: 0.4), { state in + var updatedState = state + updatedState.addressFocused = true + return updatedState + }) + case .closeAddressBar: + self.updatePresentationState(transition: .spring(duration: 0.4), { state in + var updatedState = state + updatedState.addressFocused = false + return updatedState + }) + case let .navigateTo(address, addToRecent): + if let content = self.content.last as? BrowserWebContent { + content.navigateTo(address: address) + if addToRecent { + content.addToRecentlyVisited() + } + } + self.updatePresentationState(transition: .spring(duration: 0.4), { state in + var updatedState = state + updatedState.addressFocused = false + return updatedState + }) + case .expand: + if let content = self.content.last { + content.resetScrolling() + } } } + + self.presentationDataDisposable = (controller.context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + guard let self else { + return + } + self.presentationData = presentationData + for content in self.content { + content.updatePresentationData(presentationData) + } + self.requestLayout(transition: .immediate) + }) } deinit { - self.contentStateDisposable?.dispose() + self.presentationDataDisposable?.dispose() + self.contentStateDisposable.dispose() } override func didLoad() { super.didLoad() + self.contentContainerView.clipsToBounds = true self.view.addSubview(self.contentContainerView) - if let content = self.content { - self.contentContainerView.addSubview(content) - } } - func updatePresentationState(animated: Bool = false, _ f: (BrowserPresentationState) -> BrowserPresentationState) { + func updatePresentationState(transition: ComponentTransition = .immediate, _ f: (BrowserPresentationState) -> BrowserPresentationState) { self.presentationState = f(self.presentationState) - self.requestLayout(transition: animated ? .easeInOut(duration: 0.2) : .immediate) + self.requestLayout(transition: transition) + } + + func pushContent(_ content: BrowserScreen.Subject, transition: ComponentTransition) { + let browserContent: BrowserContent + switch content { + case let .webPage(url): + let webContent = BrowserWebContent(context: self.context, presentationData: self.presentationData, url: url, preferredConfiguration: self.controller?.preferredConfiguration) + webContent.cancelInteractiveTransitionGestures = { [weak self] in + if let self, let view = self.controller?.view { + cancelInteractiveTransitionGestures(view: view) + } + } + browserContent = webContent + self.controller?.preferredConfiguration = nil + case let .instantPage(webPage, anchor, sourceLocation): + let instantPageContent = BrowserInstantPageContent(context: self.context, presentationData: self.presentationData, webPage: webPage, anchor: anchor, url: webPage.content.url ?? "", sourceLocation: sourceLocation) + instantPageContent.openPeer = { [weak self] peer in + guard let self else { + return + } + self.openPeer(peer) + } + browserContent = instantPageContent + case let .document(file): + browserContent = BrowserDocumentContent(context: self.context, presentationData: self.presentationData, file: file) + case let .pdfDocument(file): + browserContent = BrowserPdfContent(context: self.context, presentationData: self.presentationData, file: file) + } + browserContent.pushContent = { [weak self] content in + guard let self else { + return + } + self.pushContent(content, transition: .spring(duration: 0.4)) + } + browserContent.openAppUrl = { [weak self] url in + guard let self else { + return + } + self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: false, presentationData: self.presentationData, navigationController: self.controller?.navigationController as? NavigationController, dismissInput: { [weak self] in + self?.view.window?.endEditing(true) + }) + } + browserContent.present = { [weak self] c, a in + guard let self, let controller = self.controller else { + return + } + controller.present(c, in: .window(.root), with: a) + } + browserContent.presentInGlobalOverlay = { [weak self] c in + guard let self, let controller = self.controller else { + return + } + controller.presentInGlobalOverlay(c) + } + browserContent.getNavigationController = { [weak self] in + return self?.controller?.navigationController as? NavigationController + } + browserContent.minimize = { [weak self] in + guard let self else { + return + } + self.minimize() + } + browserContent.close = { [weak self] in + guard let self, let controller = self.controller else { + return + } + if controller.isMinimized { + if let navigationController = controller.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer { + minimizedContainer.removeController(controller) + } + } else { + controller.dismiss() + } + } + + self.content.append(browserContent) + self.requestLayout(transition: transition) + + self.setupContentStateUpdates() + } + + func popContent(transition: ComponentTransition) { + self.content.removeLast() + self.requestLayout(transition: transition) + + self.setupContentStateUpdates() } - func minimize() { + func openPeer(_ peer: EnginePeer) { guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { return } - navigationController.minimizeViewController(controller, damping: nil, beforeMaximize: { _, completion in + self.minimize() + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), animated: true)) + } + + func addBookmark(_ url: String, showArrow: Bool) { + let _ = enqueueMessages( + account: self.context.account, + peerId: self.context.account.peerId, + messages: [.message( + text: url, + attributes: [], + inlineStickers: [:], + mediaReference: nil, + threadId: nil, + replyToMessageId: nil, + replyToStoryId: nil, + localGroupingKey: nil, + correlationId: nil, + bubbleUpEmojiOrStickersets: [] + )] + ).start() + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + let lastController = self.controller?.navigationController?.viewControllers.last as? ViewController + lastController?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: presentationData.strings.WebBrowser_LinkAddedToBookmarks), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in + if let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.minimize() + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) + }) + } + return false + }), in: .current) + } + + private func setupContentStateUpdates() { + for content in self.content { + content.onScrollingUpdate = { _ in } + } + + guard let content = self.content.last else { + self.controller?.title = "" + self.contentState = nil + self.contentStateDisposable.set(nil) + self.requestLayout(transition: .easeInOut(duration: 0.25)) + return + } + + var previousState = BrowserContentState(title: "", url: "", estimatedProgress: 1.0, readingProgress: 0.0, contentType: .webPage, canGoBack: false, canGoForward: false, backList: [], forwardList: []) + if self.content.count > 1 { + for content in self.content.prefix(upTo: self.content.count - 1) { + var backList = previousState.backList + backList.append(BrowserContentState.HistoryItem(url: content.currentState.url, title: content.currentState.title, uuid: content.uuid)) + previousState = previousState.withUpdatedBackList(backList) + } + } + + self.contentStateDisposable.set((content.state + |> deliverOnMainQueue).startStrict(next: { [weak self] state in + guard let self else { + return + } + var backList = state.backList + backList.insert(contentsOf: previousState.backList, at: 0) + + var canGoBack = state.canGoBack + if !backList.isEmpty { + canGoBack = true + } + + let previousState = self.contentState + let state = state.withUpdatedCanGoBack(canGoBack).withUpdatedBackList(backList) + self.controller?.title = state.title + self.contentState = state + + if !self.isUpdating { + let transition: ComponentTransition + if let previousState, previousState.withUpdatedReadingProgress(state.readingProgress) == state { + transition = .immediate + } else { + transition = .easeInOut(duration: 0.25) + } + self.requestLayout(transition: transition) + } + })) + + content.onScrollingUpdate = { [weak self] update in + self?.onContentScrollingUpdate(update) + } + } + + func minimize(topEdgeOffset: CGFloat? = nil, damping: CGFloat? = nil, initialVelocity: CGFloat? = nil) { + guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { + return + } + navigationController.minimizeViewController(controller, topEdgeOffset: topEdgeOffset, damping: damping, velocity: initialVelocity, beforeMaximize: { _, completion in completion() }, setupContainer: { [weak self] current in let minimizedContainer: MinimizedContainerImpl? @@ -462,10 +991,28 @@ public class BrowserScreen: ViewController { }, animated: true) } + func openBookmarks() { + guard let url = self.contentState?.url else { + return + } + let controller = BrowserBookmarksScreen(context: self.context, url: url, openUrl: { [weak self] url in + if let self { + self.performAction.invoke(.navigateTo(url, true)) + } + }, addBookmark: { [weak self] in + self?.addBookmark(url, showArrow: false) + }) + self.controller?.push(controller) + } + func openSettings() { guard let referenceView = self.componentHost.findTaggedView(tag: settingsTag) as? ReferenceButtonComponent.View else { return } + + if let animationComponentView = referenceView.componentView.view as? LottieComponent.View { + animationComponentView.playOnce() + } self.view.endEditing(true) @@ -486,7 +1033,7 @@ public class BrowserScreen: ViewController { let _ = (settings |> deliverOnMainQueue).start(next: { [weak self] settings in - guard let self, let controller = self.controller else { + guard let self, let controller = self.controller, let contentState = self.contentState, let layout = self.validLayout?.0 else { return } @@ -494,20 +1041,20 @@ public class BrowserScreen: ViewController { let performAction = self.performAction - let forceIsSerif = self.presentationState.fontIsSerif + let forceIsSerif = self.presentationState.fontState.isSerif let fontItem = BrowserFontSizeContextMenuItem( - value: self.presentationState.fontSize, + value: self.presentationState.fontState.size, decrease: { [weak self] in performAction.invoke(.decreaseFontSize) if let self { - return self.presentationState.fontSize + return self.presentationState.fontState.size } else { return 100 } }, increase: { [weak self] in performAction.invoke(.increaseFontSize) if let self { - return self.presentationState.fontSize + return self.presentationState.fontState.size } else { return 100 } @@ -521,7 +1068,7 @@ public class BrowserScreen: ViewController { defaultWebBrowser = "safari" } - let url = self.contentState?.url ?? "" + let url = contentState.url let openInOptions = availableOpenInOptions(context: self.context, item: .url(url: url)) let openInTitle: String let openInUrl: String @@ -537,27 +1084,59 @@ public class BrowserScreen: ViewController { openInUrl = url } - let items: [ContextMenuItem] = [ - .custom(fontItem, false), - .action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontSanFrancisco, icon: forceIsSerif ? emptyIcon : checkIcon, action: { (controller, action) in + + let canOpenIn = !(self.contentState?.url.hasPrefix("tonsite") ?? false) + + var items: [ContextMenuItem] = [] + 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) - })), .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) - })), - .separator, - .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 + }))) + + 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 + performAction.invoke(.reload) + action(.default) + }))) + } + if [.webPage].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) - })), - .action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in - if let self { - self.context.sharedContext.applicationBindings.openUrl(openInUrl) - } + }))) + } + + if !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) - })) - ] + }))) + } + + if [.webPage, .instantPage].contains(contentState.contentType) { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_AddBookmark, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in + performAction.invoke(.addBookmark) + action(.default) + }))) + if !layout.metrics.isTablet && canOpenIn { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in + if let self { + self.context.sharedContext.applicationBindings.openUrl(openInUrl) + } + action(.default) + }))) + } + } let contextController = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items)))) self.controller?.present(contextController, in: .window(.root)) @@ -566,7 +1145,7 @@ public class BrowserScreen: ViewController { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) - if result == self.componentHost.view, let content = self.content { + if result == self.componentHost.view, let content = self.content.last { return content.hitTest(self.view.convert(point, to: content), with: event) } return result @@ -615,6 +1194,10 @@ public class BrowserScreen: ViewController { } } + if update.isReset { + scrollingPanelOffsetFraction = 0.0 + } + if scrollingPanelOffsetFraction != self.scrollingPanelOffsetFraction { self.scrollingPanelOffsetFraction = scrollingPanelOffsetFraction self.requestLayout(transition: transition) @@ -622,13 +1205,64 @@ public class BrowserScreen: ViewController { } } + func navigateTo(_ item: BrowserContentState.HistoryItem) { + if let _ = item.webItem { + if let last = self.content.last { + last.navigateTo(historyItem: item) + } + } else if let uuid = item.uuid { + var newContent = self.content + while newContent.last?.uuid != uuid { + newContent.removeLast() + } + self.content = newContent + self.requestLayout(transition: .spring(duration: 0.4)) + } + } + + func performHoldAction(view: UIView, gesture: ContextGesture?, action: BrowserScreen.Action) { + guard let controller = self.controller, let contentState = self.contentState else { + return + } + + let source: ContextContentSource = .reference(BrowserReferenceContentSource(controller: controller, sourceView: view)) + var items: [ContextMenuItem] = [] + switch action { + case .navigateBack: + for item in contentState.backList { + items.append(.action(ContextMenuActionItem(text: item.title, textLayout: .secondLineWithValue(item.url), icon: { _ in return nil }, action: { [weak self] (_, action) in + self?.navigateTo(item) + action(.default) + }))) + } + case .navigateForward: + for item in contentState.forwardList { + items.append(.action(ContextMenuActionItem(text: item.title, textLayout: .secondLineWithValue(item.url), icon: { _ in return nil }, action: { [weak self] (_, action) in + self?.navigateTo(item) + action(.default) + }))) + } + default: + return + } + + let contextController = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items)))) + self.controller?.present(contextController, in: .window(.root)) + } + + private var isUpdating = false func requestLayout(transition: ComponentTransition) { - if let (layout, navigationBarHeight) = self.validLayout { + if !self.isUpdating, let (layout, navigationBarHeight) = self.validLayout { self.containerLayoutUpdated(layout: layout, navigationBarHeight: navigationBarHeight, transition: transition) } } func containerLayoutUpdated(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ComponentTransition) { + self.isUpdating = true + defer { + self.isUpdating = false + } + self.validLayout = (layout, navigationBarHeight) let environment = ViewControllerComponentContainer.Environment( @@ -662,6 +1296,11 @@ public class BrowserScreen: ViewController { contentState: self.contentState, presentationState: self.presentationState, performAction: self.performAction, + performHoldAction: { [weak self] view, gesture, action in + if let self { + self.performHoldAction(view: view, gesture: gesture, action: action) + } + }, panelCollapseFraction: self.scrollingPanelOffsetFraction ) ), @@ -679,12 +1318,49 @@ public class BrowserScreen: ViewController { transition.setFrame(view: componentView, frame: CGRect(origin: .zero, size: componentSize)) } transition.setFrame(view: self.contentContainerView, frame: CGRect(origin: .zero, size: layout.size)) - if let content = self.content { - let collapsedHeight: CGFloat = 24.0 - let topInset: CGFloat = environment.statusBarHeight + navigationBarHeight * (1.0 - self.scrollingPanelOffsetFraction) + collapsedHeight * self.scrollingPanelOffsetFraction - let bottomInset = layout.intrinsicInsets.bottom - content.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: bottomInset, right: layout.safeInsets.right), transition: transition) - transition.setFrame(view: content, frame: CGRect(origin: .zero, size: layout.size)) + + var items: [AnyComponentWithIdentity] = [] + for content in self.content { + items.append( + AnyComponentWithIdentity(id: content.uuid, component: AnyComponent( + BrowserContentComponent( + content: content, + insets: UIEdgeInsets( + top: layout.statusBarHeight ?? 0.0, + left: layout.safeInsets.left, + bottom: layout.intrinsicInsets.bottom, + right: layout.safeInsets.right + ), + navigationBarHeight: navigationBarHeight, + scrollingPanelOffsetFraction: self.scrollingPanelOffsetFraction, + hasBottomPanel: !layout.metrics.isTablet || self.presentationState.isSearching + ) + )) + ) + } + + let _ = self.contentNavigationContainer.update( + transition: transition, + component: AnyComponent( + NavigationStackComponent( + items: items, + requestPop: { [weak self] in + guard let self else { + return + } + self.popContent(transition: .spring(duration: 0.4)) + } + ) + ), + environment: {}, + containerSize: layout.size + ) + let navigationFrame = CGRect(origin: .zero, size: layout.size) + if let view = self.contentNavigationContainer.view { + if view.superview == nil { + self.contentContainerView.addSubview(view) + } + transition.setFrame(view: view, frame: navigationFrame) } self.navigationBarHeight = environment.navigationHeight @@ -694,23 +1370,54 @@ public class BrowserScreen: ViewController { public enum Subject { case webPage(url: String) + case instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) + case document(file: TelegramMediaFile) + case pdfDocument(file: TelegramMediaFile) } private let context: AccountContext private let subject: Subject + private var preferredConfiguration: WKWebViewConfiguration? + private var openPreviousOnClose = false - public init(context: AccountContext, subject: Subject) { + 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" + ] + + public init(context: AccountContext, subject: Subject, preferredConfiguration: WKWebViewConfiguration? = nil, openPreviousOnClose: Bool = false) { + var subject = subject + if case let .webPage(url) = subject, let parsedUrl = URL(string: url) { + if parsedUrl.host?.hasSuffix(".ton") == true { + var urlComponents = URLComponents(string: url) + urlComponents?.scheme = "tonsite" + if let updatedUrl = urlComponents?.url?.absoluteString { + subject = .webPage(url: updatedUrl) + } + } + } self.context = context self.subject = subject + self.preferredConfiguration = preferredConfiguration + self.openPreviousOnClose = openPreviousOnClose super.init(navigationBarPresentationData: nil) - self.navigationPresentation = .modal + self.navigationPresentation = .modalInCompactLayout self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown) self.scrollToTop = { [weak self] in - (self?.displayNode as? Node)?.content?.scrollToTop() + self?.node.content.last?.scrollToTop() } } @@ -718,6 +1425,10 @@ public class BrowserScreen: ViewController { preconditionFailure() } + var node: Node { + return self.displayNode as! Node + } + override public func loadDisplayNode() { self.displayNode = Node(controller: self) @@ -725,9 +1436,93 @@ public class BrowserScreen: ViewController { } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + super.containerLayoutUpdated(layout, transition: transition) - (self.displayNode as! Node).containerLayoutUpdated(layout: layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.height, transition: ComponentTransition(transition)) + var navigationHeight = self.navigationLayout(layout: layout).navigationFrame.height + if layout.metrics.isTablet, layout.size.width > layout.size.height { + navigationHeight += 6.0 + } + self.node.containerLayoutUpdated(layout: layout, navigationBarHeight: navigationHeight, transition: ComponentTransition(transition)) + } + + public func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) { + self.openPreviousOnClose = false + self.node.minimize(topEdgeOffset: topEdgeOffset, damping: 180.0, initialVelocity: initialVelocity) + } + + private var didPlayAppearanceAnimation = false + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if !self.didPlayAppearanceAnimation, let layout = self.validLayout, layout.metrics.isTablet { + self.node.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } + + public override func dismiss(completion: (() -> Void)? = nil) { + if let layout = self.validLayout, layout.metrics.isTablet { + self.node.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: layout.size.height), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in + super.dismiss(completion: completion) + }) + } else { + super.dismiss(completion: completion) + } + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if self.openPreviousOnClose, let navigationController = self.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer, let controller = minimizedContainer.controllers.last { + navigationController.maximizeViewController(controller, animated: true) + } + } + + public var isMinimized = false { + didSet { + if let webContent = self.node.content.last as? BrowserWebContent { + if !self.isMinimized { + webContent.webView.setNeedsLayout() + } + } + } + } + public var isMinimizable = true + + public var minimizedIcon: UIImage? { + if let contentState = self.node.contentState { + switch contentState.contentType { + case .webPage: + return contentState.favicon + case .instantPage: + return UIImage(bundleImageName: "Chat/Message/AttachedContentInstantIcon")?.withRenderingMode(.alwaysTemplate) + case .document: + return nil + } + } + return nil + } + + public var minimizedProgress: Float? { + if let contentState = self.node.contentState { + return Float(contentState.readingProgress) + } + return nil + } + + public func makeContentSnapshotView() -> UIView? { + if let contentSnapshot = self.node.content.last?.makeContentSnapshotView(), let layout = self.validLayout { + if let wrapperView = self.view.snapshotView(afterScreenUpdates: false) { + contentSnapshot.frame = contentSnapshot.frame.offsetBy(dx: 0.0, dy: self.navigationLayout(layout: layout).navigationFrame.height) + wrapperView.addSubview(contentSnapshot) + return wrapperView + } else { + return contentSnapshot + } + } else { + return self.view.snapshotView(afterScreenUpdates: false) + } } } @@ -744,3 +1539,95 @@ private final class BrowserReferenceContentSource: ContextReferenceContentSource return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) } } + +private final class BrowserContentComponent: Component { + let content: BrowserContent + let insets: UIEdgeInsets + let navigationBarHeight: CGFloat + let scrollingPanelOffsetFraction: CGFloat + let hasBottomPanel: Bool + + init( + content: BrowserContent, + insets: UIEdgeInsets, + navigationBarHeight: CGFloat, + scrollingPanelOffsetFraction: CGFloat, + hasBottomPanel: Bool + ) { + self.content = content + self.insets = insets + self.navigationBarHeight = navigationBarHeight + self.scrollingPanelOffsetFraction = scrollingPanelOffsetFraction + self.hasBottomPanel = hasBottomPanel + } + + static func ==(lhs: BrowserContentComponent, rhs: BrowserContentComponent) -> Bool { + if lhs.content.uuid != rhs.content.uuid { + return false + } + if lhs.insets != rhs.insets { + return false + } + if lhs.navigationBarHeight != rhs.navigationBarHeight { + return false + } + if lhs.scrollingPanelOffsetFraction != rhs.scrollingPanelOffsetFraction { + return false + } + if lhs.hasBottomPanel != rhs.hasBottomPanel { + return false + } + return true + } + + final class View: UIView { + init() { + super.init(frame: CGRect()) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: BrowserContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + if component.content.superview !== self { + self.addSubview(component.content) + } + + let collapsedHeight: CGFloat = 24.0 + let topInset: CGFloat = component.navigationBarHeight * (1.0 - component.scrollingPanelOffsetFraction) + (component.insets.top + collapsedHeight) * component.scrollingPanelOffsetFraction + let bottomInset = component.hasBottomPanel ? (49.0 + component.insets.bottom) * (1.0 - component.scrollingPanelOffsetFraction) : 0.0 + let insets = UIEdgeInsets(top: topInset, left: component.insets.left, bottom: bottomInset, right: component.insets.right) + let fullInsets = UIEdgeInsets(top: component.insets.top + component.navigationBarHeight, left: component.insets.left, bottom: component.hasBottomPanel ? 49.0 + component.insets.bottom : 0.0, right: component.insets.right) + + component.content.updateLayout(size: availableSize, insets: insets, fullInsets: fullInsets, safeInsets: component.insets, transition: transition) + transition.setFrame(view: component.content, frame: CGRect(origin: .zero, size: availableSize)) + + return availableSize + } + } + + 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) + } +} + +private func cancelInteractiveTransitionGestures(view: UIView) { + if let gestureRecognizers = view.gestureRecognizers { + for gesture in gestureRecognizers { + if let gesture = gesture as? InteractiveTransitionGestureRecognizer { + gesture.cancel() + } else if let scrollView = gesture.view as? UIScrollView, gesture.isEnabled, scrollView.tag == 0x5C4011 { + gesture.isEnabled = false + gesture.isEnabled = true + } + } + } + if let superview = view.superview { + cancelInteractiveTransitionGestures(view: superview) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift b/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift index c49fa13540a..6e8974667fa 100644 --- a/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift @@ -8,6 +8,8 @@ import AccountContext import BundleIconComponent final class SearchBarContentComponent: Component { + public typealias EnvironmentType = BrowserNavigationBarEnvironment + let theme: PresentationTheme let strings: PresentationStrings let performAction: ActionSlot @@ -33,7 +35,7 @@ final class SearchBarContentComponent: Component { } final class View: UIView, UITextFieldDelegate { - private final class EmojiSearchTextField: UITextField { + private final class SearchTextField: UITextField { override func textRect(forBounds bounds: CGRect) -> CGRect { return bounds.integral } @@ -75,7 +77,7 @@ final class SearchBarContentComponent: Component { private var placeholderContent = ComponentView() private var textFrame: CGRect? - private var textField: EmojiSearchTextField? + private var textField: SearchTextField? private var tapRecognizer: UITapGestureRecognizer? @@ -160,7 +162,7 @@ final class SearchBarContentComponent: Component { 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 textField = EmojiSearchTextField(frame: textFieldFrame) + let textField = SearchTextField(frame: textFieldFrame) textField.autocorrectionType = .no textField.returnKeyType = .search self.textField = textField @@ -285,7 +287,7 @@ final class SearchBarContentComponent: Component { component: AnyComponent(Text( text: strings.Common_Cancel, font: Font.regular(17.0), - color: theme.rootController.navigationBar.primaryTextColor + color: theme.rootController.navigationBar.accentTextColor )), environment: {}, containerSize: CGSize(width: size.width - 32.0, height: 100.0) @@ -351,7 +353,7 @@ final class SearchBarContentComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + 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/BrowserUI/Sources/BrowserTitleBarComponent.swift b/submodules/BrowserUI/Sources/BrowserTitleBarComponent.swift new file mode 100644 index 00000000000..a362da7d610 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserTitleBarComponent.swift @@ -0,0 +1,85 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramPresentationData +import AccountContext +import BundleIconComponent +import MultilineTextComponent +import UrlEscaping + +final class TitleBarContentComponent: Component { + public typealias EnvironmentType = BrowserNavigationBarEnvironment + + let theme: PresentationTheme + let title: String + + init( + theme: PresentationTheme, + title: String + ) { + self.theme = theme + self.title = title + } + + static func ==(lhs: TitleBarContentComponent, rhs: TitleBarContentComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + final class View: UIView { + private var titleContent = ComponentView() + private var component: TitleBarContentComponent? + + init() { + super.init(frame: CGRect()) + } + + required public init?(coder: NSCoder) { + fatalError() + } + + func update(component: TitleBarContentComponent, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let titleSize = self.titleContent.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center, + truncationType: .end, + maximumNumberOfLines: 1 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - 36.0, height: availableSize.height) + ) + let titleContentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - titleSize.height) / 2.0)), size: titleSize) + if let titleContentView = self.titleContent.view { + if titleContentView.superview == nil { + self.addSubview(titleContentView) + } + transition.setPosition(view: titleContentView, position: titleContentFrame.center) + titleContentView.bounds = CGRect(origin: .zero, size: titleContentFrame.size) + } + + return availableSize + } + } + + 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, environment: environment, transition: transition) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift b/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift index ad57dc233e3..4c1e2c5a32c 100644 --- a/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift @@ -5,6 +5,7 @@ import ComponentFlow import BlurredBackgroundComponent import BundleIconComponent import TelegramPresentationData +import ContextReferenceButtonComponent final class BrowserToolbarComponent: CombinedComponent { let backgroundColor: UIColor @@ -119,24 +120,36 @@ final class BrowserToolbarComponent: CombinedComponent { } final class NavigationToolbarContentComponent: CombinedComponent { + let accentColor: UIColor let textColor: UIColor let canGoBack: Bool let canGoForward: Bool + let canOpenIn: Bool let performAction: ActionSlot + let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void init( + accentColor: UIColor, textColor: UIColor, canGoBack: Bool, canGoForward: Bool, - performAction: ActionSlot + canOpenIn: Bool, + performAction: ActionSlot, + performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void ) { + self.accentColor = accentColor self.textColor = textColor self.canGoBack = canGoBack self.canGoForward = canGoForward + self.canOpenIn = canOpenIn self.performAction = performAction + self.performHoldAction = performHoldAction } static func ==(lhs: NavigationToolbarContentComponent, rhs: NavigationToolbarContentComponent) -> Bool { + if lhs.accentColor != rhs.accentColor { + return false + } if lhs.textColor != rhs.textColor { return false } @@ -146,36 +159,55 @@ final class NavigationToolbarContentComponent: CombinedComponent { if lhs.canGoForward != rhs.canGoForward { return false } + if lhs.canOpenIn != rhs.canOpenIn { + return false + } return true } static var body: Body { - let back = Child(Button.self) - let forward = Child(Button.self) + let back = Child(ContextReferenceButtonComponent.self) + let forward = Child(ContextReferenceButtonComponent.self) let share = Child(Button.self) + let bookmark = Child(Button.self) let openIn = Child(Button.self) return { context in let availableSize = context.availableSize let performAction = context.component.performAction - + let performHoldAction = context.component.performHoldAction + let sideInset: CGFloat = 5.0 let buttonSize = CGSize(width: 50.0, height: availableSize.height) - let spacing = (availableSize.width - buttonSize.width * 4.0 - sideInset * 2.0) / 3.0 + var buttonCount = 4 + 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: Button( + component: ContextReferenceButtonComponent( content: AnyComponent( BundleIconComponent( name: "Instant View/Back", - tintColor: context.component.textColor + tintColor: canGoBack ? context.component.accentColor : context.component.accentColor.withAlphaComponent(0.4) ) ), - isEnabled: context.component.canGoBack, - action: { - performAction.invoke(.navigateBack) + minSize: buttonSize, + action: { view, gesture in + guard canGoBack else { + return + } + if let gesture { + performHoldAction(view, gesture, .navigateBack) + } else { + performAction.invoke(.navigateBack) + } } - ).minSize(buttonSize), + ), availableSize: buttonSize, transition: .easeInOut(duration: 0.2) ) @@ -183,19 +215,27 @@ final class NavigationToolbarContentComponent: CombinedComponent { .position(CGPoint(x: sideInset + back.size.width / 2.0, y: availableSize.height / 2.0)) ) + let canGoForward = context.component.canGoForward let forward = forward.update( - component: Button( + component: ContextReferenceButtonComponent( content: AnyComponent( BundleIconComponent( name: "Instant View/Forward", - tintColor: context.component.textColor + tintColor: canGoForward ? context.component.accentColor : context.component.accentColor.withAlphaComponent(0.4) ) ), - isEnabled: context.component.canGoForward, - action: { - performAction.invoke(.navigateForward) + minSize: buttonSize, + action: { view, gesture in + guard canGoForward else { + return + } + if let gesture { + performHoldAction(view, gesture, .navigateForward) + } else { + performAction.invoke(.navigateForward) + } } - ).minSize(buttonSize), + ), availableSize: buttonSize, transition: .easeInOut(duration: 0.2) ) @@ -208,7 +248,7 @@ final class NavigationToolbarContentComponent: CombinedComponent { content: AnyComponent( BundleIconComponent( name: "Chat List/NavigationShare", - tintColor: context.component.textColor + tintColor: context.component.accentColor ) ), action: { @@ -222,25 +262,46 @@ final class NavigationToolbarContentComponent: CombinedComponent { .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width / 2.0, y: availableSize.height / 2.0)) ) - let openIn = openIn.update( + let bookmark = bookmark.update( component: Button( content: AnyComponent( BundleIconComponent( - name: "Instant View/Minimize", - tintColor: context.component.textColor + name: "Instant View/Bookmark", + tintColor: context.component.accentColor ) ), action: { - performAction.invoke(.minimize) + performAction.invoke(.openBookmarks) } ).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 + openIn.size.width / 2.0, y: availableSize.height / 2.0)) + 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 } } @@ -367,4 +428,3 @@ final class SearchToolbarContentComponent: CombinedComponent { } } } - diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 9f227afc8be..de137be6504 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -1,34 +1,53 @@ import Foundation import UIKit +import Display import ComponentFlow import TelegramCore import Postbox import SwiftSignalKit import TelegramPresentationData import TelegramUIPreferences +import PresentationDataUtils import AccountContext import WebKit import AppBundle +import PromptUI +import SafariServices +import ShareController +import UndoUI +import LottieComponent +import MultilineTextComponent +import UrlEscaping +import UrlHandling +import SaveProgressScreen +import DeviceModel -private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler { +private final class TonSchemeHandler: NSObject, WKURLSchemeHandler { private final class PendingTask { let sourceTask: any WKURLSchemeTask var urlSessionTask: URLSessionTask? let isCompleted = Atomic(value: false) - init(sourceTask: any WKURLSchemeTask) { + init(proxyServerHost: String, sourceTask: any WKURLSchemeTask) { self.sourceTask = sourceTask - var cleanUrl = sourceTask.request.url!.absoluteString - if let range = cleanUrl.range(of: "/ipfs/") { - cleanUrl = "ipfs://" + String(cleanUrl[range.upperBound...]) - } else if let range = cleanUrl.range(of: "/ipns/") { - cleanUrl = "ipns://" + String(cleanUrl[range.upperBound...]) - } - print("Load: \(cleanUrl)") - cleanUrl = cleanUrl.replacingOccurrences(of: "ipns://", with: "ipns/") - cleanUrl = cleanUrl.replacingOccurrences(of: "ipfs://", with: "ipfs/") - let mappedUrl = "https://cloudflare-ipfs.com/\(cleanUrl)" + let requestUrl = sourceTask.request.url + + var mappedHost: String = "" + if let host = sourceTask.request.url?.host { + mappedHost = host + mappedHost = mappedHost.replacingOccurrences(of: "-", with: "-h") + mappedHost = mappedHost.replacingOccurrences(of: ".", with: "-d") + } + + var mappedPath = "" + if let path = sourceTask.request.url?.path, !path.isEmpty { + mappedPath = path + if !path.hasPrefix("/") { + mappedPath = "/\(mappedPath)" + } + } + let mappedUrl = "https://\(mappedHost).\(proxyServerHost)\(mappedPath)" let isCompleted = self.isCompleted self.urlSessionTask = URLSession.shared.dataTask(with: URLRequest(url: URL(string: mappedUrl)!), completionHandler: { data, response, error in if isCompleted.swap(true) { @@ -39,7 +58,20 @@ private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler { sourceTask.didFailWithError(error) } else { if let response { - sourceTask.didReceive(response) + if let response = response as? HTTPURLResponse, let requestUrl { + if let updatedResponse = HTTPURLResponse( + url: requestUrl, + statusCode: response.statusCode, + httpVersion: "HTTP/1.1", + headerFields: response.allHeaderFields as? [String: String] ?? [:] + ) { + sourceTask.didReceive(updatedResponse) + } else { + sourceTask.didReceive(response) + } + } else { + sourceTask.didReceive(response) + } } if let data { sourceTask.didReceive(data) @@ -65,10 +97,16 @@ private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler { } } + private let proxyServerHost: String + private var pendingTasks: [PendingTask] = [] + init(proxyServerHost: String) { + self.proxyServerHost = proxyServerHost + } + func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) { - self.pendingTasks.append(PendingTask(sourceTask: urlSchemeTask)) + self.pendingTasks.append(PendingTask(proxyServerHost: self.proxyServerHost, sourceTask: urlSchemeTask)) } func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) { @@ -80,55 +118,207 @@ private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler { } } -final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { - private let webView: WKWebView +final class WebView: WKWebView { + var customBottomInset: CGFloat = 0.0 { + didSet { + self.setNeedsLayout() + } + } + + override var safeAreaInsets: UIEdgeInsets { + return UIEdgeInsets(top: 0.0, left: 0.0, bottom: self.customBottomInset, right: 0.0) + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + var result = super.point(inside: point, with: event) + if !result && point.x > 0.0 && point.y < self.frame.width && point.y > 0.0 && point.y < self.frame.height + 83.0 { + result = true + } + return result + } +} + +private class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler { + private let f: (WKScriptMessage) -> () + + init(_ f: @escaping (WKScriptMessage) -> ()) { + self.f = f + + super.init() + } + + func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) { + self.f(scriptMessage) + } +} + +private func computedUserAgent() -> String { + func getFirmwareVersion() -> String? { + var size = 0 + sysctlbyname("kern.osversion", nil, &size, nil, 0) + + var str = [CChar](repeating: 0, count: size) + sysctlbyname("kern.osversion", &str, &size, nil, 0) + + return String(cString: str) + } + + let osVersion = UIDevice.current.systemVersion + let firmwareVersion = getFirmwareVersion() ?? "15E148" + return DeviceModel.current.isIpad ? "Version/\(osVersion) Safari/605.1.15" : "Version/\(osVersion) Mobile/\(firmwareVersion) Safari/604.1" +} + +final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { + private let context: AccountContext + private var presentationData: PresentationData + + let webView: WebView + + private let errorView: ComponentHostView + private var currentError: Error? + + let uuid: UUID private var _state: BrowserContentState private let statePromise: Promise + var currentState: BrowserContentState { + return self._state + } var state: Signal { return self.statePromise.get() } + private let faviconDisposable = MetaDisposable() + + var pushContent: (BrowserScreen.Subject) -> Void = { _ in } + var openAppUrl: (String) -> Void = { _ in } var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } + var minimize: () -> Void = { } + var close: () -> Void = { } + var present: (ViewController, Any?) -> Void = { _, _ in } + var presentInGlobalOverlay: (ViewController) -> Void = { _ in } + var getNavigationController: () -> NavigationController? = { return nil } + var cancelInteractiveTransitionGestures: () -> Void = {} - init(context: AccountContext, url: String) { - let configuration = WKWebViewConfiguration() - - if context.sharedContext.immediateExperimentalUISettings.browserExperiment { - configuration.setURLSchemeHandler(IpfsSchemeHandler(), forURLScheme: "ipns") - configuration.setURLSchemeHandler(IpfsSchemeHandler(), forURLScheme: "ipfs") - } + private var tempFile: TempBoxFile? + + init(context: AccountContext, presentationData: PresentationData, url: String, preferredConfiguration: WKWebViewConfiguration? = nil) { + self.context = context + self.uuid = UUID() + self.presentationData = presentationData - self.webView = WKWebView(frame: CGRect(), configuration: configuration) - self.webView.allowsLinkPreview = false + var handleScriptMessageImpl: ((WKScriptMessage) -> Void)? - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + let configuration: WKWebViewConfiguration + if let preferredConfiguration { + configuration = preferredConfiguration + } else { + configuration = WKWebViewConfiguration() + var proxyServerHost = "magic.org" + if let data = context.currentAppConfiguration.with({ $0 }).data, let hostValue = data["ton_proxy_address"] as? String { + proxyServerHost = hostValue + } + configuration.setURLSchemeHandler(TonSchemeHandler(proxyServerHost: proxyServerHost), forURLScheme: "tonsite") + configuration.allowsInlineMediaPlayback = true + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + configuration.mediaTypesRequiringUserActionForPlayback = [] + } else { + configuration.mediaPlaybackRequiresUserAction = false + } + + let contentController = WKUserContentController() + let videoScript = WKUserScript(source: videoSource, injectionTime: .atDocumentStart, forMainFrameOnly: false) + contentController.addUserScript(videoScript) + let touchScript = WKUserScript(source: setupTouchObservers, injectionTime: .atDocumentStart, forMainFrameOnly: false) + contentController.addUserScript(touchScript) + + let eventProxyScript = WKUserScript(source: eventProxySource, injectionTime: .atDocumentStart, forMainFrameOnly: false) + contentController.addUserScript(eventProxyScript) + contentController.add(WeakScriptMessageHandler { message in + handleScriptMessageImpl?(message) + }, name: "performAction") + + configuration.userContentController = contentController + configuration.applicationNameForUserAgent = computedUserAgent() + } + + self.webView = WebView(frame: CGRect(), configuration: configuration) + self.webView.allowsLinkPreview = true + + if #available(iOS 11.0, *) { self.webView.scrollView.contentInsetAdjustmentBehavior = .never } var title: String = "" - if let parsedUrl = URL(string: url) { + if url.hasPrefix("file://") { + var updatedPath = url + let tempFile = TempBox.shared.file(path: url.replacingOccurrences(of: "file://", with: ""), fileName: "file.xlsx") + updatedPath = tempFile.path + self.tempFile = tempFile + + let request = URLRequest(url: URL(fileURLWithPath: updatedPath)) + self.webView.load(request) + } else if let parsedUrl = URL(string: url) { let request = URLRequest(url: parsedUrl) self.webView.load(request) - title = parsedUrl.host ?? "" + title = getDisplayUrl(url, hostOnly: true) } - self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, contentType: .webPage) + self.errorView = ComponentHostView() + + self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.1, readingProgress: 0.0, contentType: .webPage) self.statePromise = Promise(self._state) super.init(frame: .zero) + self.backgroundColor = presentationData.theme.list.plainBackgroundColor + self.webView.backgroundColor = presentationData.theme.list.plainBackgroundColor + self.webView.isOpaque = false + 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) self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.url), options: [], context: nil) self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: [], context: nil) self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack), options: [], context: nil) self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward), options: [], context: nil) - + self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.hasOnlySecureContent), options: [], context: nil) + if #available(iOS 15.0, *) { + self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor + } + if #available(iOS 16.4, *) { + self.webView.isInspectable = true + } self.addSubview(self.webView) + + self.webView.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in + if let self, self.webView.canGoBack { + return true + } else { + return false + } + } + + self.webView.interactiveTransitionGestureRecognizerTest = { [weak self] point in + if let self { + if let result = self.webView.hitTest(point, with: nil), let scrollView = findScrollView(view: result), scrollView.isDescendant(of: self.webView) { + if scrollView.contentSize.width > scrollView.frame.width, scrollView.contentOffset.x > -scrollView.contentInset.left { + return true + } + } + } + return false + } + + handleScriptMessageImpl = { [weak self] message in + self?.handleScriptMessage(message) + } } required init?(coder: NSCoder) { @@ -141,23 +331,52 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack)) self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward)) + self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.hasOnlySecureContent)) + + self.faviconDisposable.dispose() } - func setFontSize(_ fontSize: CGFloat) { - let js = "document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust='\(Int(fontSize * 100.0))%'" - self.webView.evaluateJavaScript(js, completionHandler: nil) + private func handleScriptMessage(_ message: WKScriptMessage) { + guard let body = message.body as? [String: Any] else { + return + } + guard let eventName = body["eventName"] as? String else { + return + } + + switch eventName { + case "cancellingTouch": + self.cancelInteractiveTransitionGestures() + default: + break + } } - func setForceSerif(_ force: Bool) { - let js: String - if force { - js = "document.getElementsByTagName(\'body\')[0].style.fontFamily = 'Georgia, serif';" - } else { - js = "document.getElementsByTagName(\'body\')[0].style.fontFamily = '\"Lucida Grande\", \"Lucida Sans Unicode\", Arial, Helvetica, Verdana, sans-serif';" + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + if #available(iOS 15.0, *) { + self.backgroundColor = presentationData.theme.list.plainBackgroundColor + self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor } - self.webView.evaluateJavaScript(js) { _, _ in + if let (size, insets, fullInsets, safeInsets) = self.validLayout { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: safeInsets, transition: .immediate) } } + + + 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) { @@ -185,65 +404,121 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { }) } + private var findSession: Any? 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) - } - }) - }) + + if #available(iOS 16.0, *), !"".isEmpty { + if let query { + var findSession: UIFindSession? + if let current = self.findSession as? UIFindSession { + findSession = current + } else { + self.webView.isFindInteractionEnabled = true + + if let findInteraction = self.webView.findInteraction, let webView = self.webView as? UIFindInteractionDelegate, let session = webView.findInteraction(findInteraction, sessionFor: self.webView) { +// session.setValue(findInteraction, forKey: "_parentInteraction") +// findInteraction.setValue(session, forKey: "_activeFindSession") + findSession = session + self.findSession = session + + webView.findInteraction?(findInteraction, didBegin: session) + } + } + if let findSession { + findSession.performSearch(query: query, options: BrowserSearchOptions()) + self.webView.findInteraction?.updateResultCount() + completion?(findSession.resultCount) + } } else { - let js = "uiWebview_RemoveAllHighlights()" - self?.webView.evaluateJavaScript(js, completionHandler: nil) - - self?.currentSearchResult = 0 - self?.searchResultsCount = 0 + if let findInteraction = self.webView.findInteraction, let webView = self.webView as? UIFindInteractionDelegate, let session = self.findSession as? UIFindSession { + webView.findInteraction?(findInteraction, didEnd: session) + self.findSession = nil + self.webView.isFindInteractionEnabled = false + } + } + } else { + self.setupSearch { [weak self] in + if let query, !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + 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 + } } } + + self.previousQuery = query } private var currentSearchResult: Int = 0 private var searchResultsCount: Int = 0 func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) { - let searchResultsCount = self.searchResultsCount - var index = self.currentSearchResult - 1 - if index < 0 { - index = searchResultsCount - 1 + 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 + + let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" + self.webView.evaluateJavaScript(js, completionHandler: { _, _ in + completion?(index, searchResultsCount) + }) } - self.currentSearchResult = index - - let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" - self.webView.evaluateJavaScript(js, completionHandler: { _, _ in - completion?(index, searchResultsCount) - }) } func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) { - let searchResultsCount = self.searchResultsCount - var index = self.currentSearchResult + 1 - if index >= searchResultsCount { - index = 0 + 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 + + let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" + self.webView.evaluateJavaScript(js, completionHandler: { _, _ in + completion?(index, searchResultsCount) + }) } - self.currentSearchResult = index - - let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" - self.webView.evaluateJavaScript(js, completionHandler: { _, _ in - completion?(index, searchResultsCount) - }) + } + + func stop() { + self.webView.stopLoading() + } + + func reload() { + self.webView.reload() } func navigateBack() { @@ -254,40 +529,99 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { 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() { self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true) } - func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) { - var scrollInsets = insets - scrollInsets.top = 0.0 - if self.webView.scrollView.contentInset != insets { - self.webView.scrollView.contentInset = scrollInsets - self.webView.scrollView.scrollIndicatorInsets = scrollInsets - } + private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets, UIEdgeInsets)? + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) { + self.validLayout = (size, insets, fullInsets, safeInsets) + self.previousScrollingOffset = ScrollingOffsetState(value: self.webView.scrollView.contentOffset.y, isDraggingOrDecelerating: self.webView.scrollView.isDragging || self.webView.scrollView.isDecelerating) - transition.setFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top))) + + let currentBounds = self.webView.scrollView.bounds + let offsetToBottomEdge = max(0.0, self.webView.scrollView.contentSize.height - currentBounds.maxY) + var bottomInset = insets.bottom + if offsetToBottomEdge < 128.0 { + bottomInset = fullInsets.bottom + } + + 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 - bottomInset)) + var refresh = false + if self.webView.frame.width > 0 && webViewFrame.width != self.webView.frame.width { + refresh = true + } + transition.setFrame(view: self.webView, frame: webViewFrame) + + if refresh { + self.webView.reloadInputViews() + } + + self.webView.customBottomInset = safeInsets.bottom * (1.0 - insets.bottom / fullInsets.bottom) + +// 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, + insets: insets + ) + ), + environment: {}, + containerSize: CGSize(width: size.width, 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) { + let updated = f(self._state) + self._state = updated + self.statePromise.set(.single(self._state)) } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - let updateState: ((BrowserContentState) -> BrowserContentState) -> Void = { f in - let updated = f(self._state) - self._state = updated - self.statePromise.set(.single(self._state)) - } - if keyPath == "title" { - updateState { $0.withUpdatedTitle(self.webView.title ?? "") } + self.updateState { $0.withUpdatedTitle(self.webView.title ?? "") } } else if keyPath == "URL" { - updateState { $0.withUpdatedUrl(self.webView.url?.absoluteString ?? "") } + if let url = self.webView.url { + self.updateState { $0.withUpdatedUrl(url.absoluteString) } + } self.didSetupSearch = false } else if keyPath == "estimatedProgress" { - updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) } + self.updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) } } else if keyPath == "canGoBack" { - updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) } - self.webView.disablesInteractiveTransitionGestureRecognizer = self.webView.canGoBack - } else if keyPath == "canGoForward" { - updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) } + self.updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) } + } else if keyPath == "canGoForward" { + self.updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) } + } else if keyPath == "hasOnlySecureContent" { + self.updateState { $0.withUpdatedIsSecure(self.webView.hasOnlySecureContent) } } } @@ -310,14 +644,25 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { self.snapScrollingOffsetToInsets() + + if self.ignoreUpdatesUntilScrollingStopped { + self.ignoreUpdatesUntilScrollingStopped = false + } } } public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { self.snapScrollingOffsetToInsets() + + if self.ignoreUpdatesUntilScrollingStopped { + self.ignoreUpdatesUntilScrollingStopped = false + } } private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) { + guard !self.ignoreUpdatesUntilScrollingStopped else { + return + } let scrollView = self.webView.scrollView let isInteracting = scrollView.isDragging || scrollView.isDecelerating if let previousScrollingOffsetValue = self.previousScrollingOffset { @@ -336,5 +681,722 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { )) } self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting) + + var readingProgress: CGFloat = 0.0 + if !scrollView.contentSize.height.isZero { + let value = (scrollView.contentOffset.y + scrollView.contentInset.top) / (scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.top) + readingProgress = max(0.0, min(1.0, value)) + } + self.updateState { + $0.withUpdatedReadingProgress(readingProgress) + } + } + + private var ignoreUpdatesUntilScrollingStopped = false + func resetScrolling() { + self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4)) + if self.webView.scrollView.isDecelerating { + self.ignoreUpdatesUntilScrollingStopped = true + } + } + + @available(iOS 13.0, *) + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { +// if #available(iOS 14.5, *), navigationAction.shouldPerformDownload { +// self.presentDownloadConfirmation(fileName: navigationAction.request.mainDocumentURL?.lastPathComponent ?? "file", proceed: { download in +// if download { +// decisionHandler(.download, preferences) +// } else { +//// decisionHandler(.cancel, preferences) +// } +// }) +// } else { + if let url = navigationAction.request.url?.absoluteString { + if (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true) && (isTelegramMeLink(url) || isTelegraPhLink(url)) && !url.contains("/auth/push?") && !self._state.url.contains("/auth/push?") { + decisionHandler(.cancel, preferences) + self.minimize() + self.openAppUrl(url) + } else { + decisionHandler(.allow, preferences) + } + } else { + decisionHandler(.allow, preferences) + } +// } + } + +// func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { +// if navigationResponse.canShowMIMEType { +// decisionHandler(.allow) +// } else if #available(iOS 14.5, *) { +// self.presentDownloadConfirmation(fileName: navigationResponse.response.suggestedFilename ?? "file", proceed: { download in +// if download { +// decisionHandler(.download) +// } else { +// decisionHandler(.cancel) +// } +// }) +// } else { +// decisionHandler(.cancel) +// } +// } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if let url = navigationAction.request.url?.absoluteString { + if (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true) && (isTelegramMeLink(url) || isTelegraPhLink(url)) { + decisionHandler(.cancel) + self.minimize() + self.openAppUrl(url) + } else { + decisionHandler(.allow) + } + } else { + decisionHandler(.allow) + } + } + + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { + if let _ = self.currentError { + self.currentError = nil + if let (size, insets, fullInsets, safeInsets) = self.validLayout { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: safeInsets, transition: .immediate) + } + } + 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) }) + } + self.parseFavicon() + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + if [-1003, -1100, 102].contains((error as NSError).code) { + self.currentError = error + } else { + self.currentError = nil + } + if let (size, insets, fullInsets, safeInsets) = self.validLayout { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: safeInsets, 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 { + if isTelegramMeLink(url) || isTelegraPhLink(url) { + self.minimize() + self.openAppUrl(url) + } else { + return self.open(url: url, configuration: configuration, new: true) + } + } + } + return nil + } + + func webViewDidClose(_ webView: WKWebView) { + Queue.mainQueue().after(0.5, { + self.close() + }) + } + + @available(iOS 15.0, *) + func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) { + decisionHandler(.prompt) + } + + func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + var completed = false + let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: message, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + if !completed { + completed = true + completionHandler() + } + })]) + alertController.dismissed = { byOutsideTap in + if byOutsideTap { + if !completed { + completed = true + completionHandler() + } + } + } + self.present(alertController, nil) + } + + func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + var completed = false + let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: message, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + if !completed { + completed = true + completionHandler(false) + } + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + if !completed { + completed = true + completionHandler(true) + } + })]) + alertController.dismissed = { byOutsideTap in + if byOutsideTap { + if !completed { + completed = true + completionHandler(false) + } + } + } + self.present(alertController, nil) + } + + func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) { + var completed = false + let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: prompt, value: defaultText, apply: { value in + if !completed { + completed = true + if let value = value { + completionHandler(value) + } else { + completionHandler(nil) + } + } + }) + promptController.dismissed = { byOutsideTap in + if byOutsideTap { + if !completed { + completed = true + completionHandler(nil) + } + } + } + self.present(promptController, nil) + } + + @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 presentDownloadConfirmation(fileName: String, proceed: @escaping (Bool) -> Void) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + var completed = false + let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: presentationData.strings.WebBrowser_Download_Confirmation(fileName).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + if !completed { + completed = true + proceed(false) + } + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_Download_Download, action: { + if !completed { + completed = true + proceed(true) + } + })]) + alertController.dismissed = { byOutsideTap in + if byOutsideTap { + if !completed { + completed = true + proceed(false) + } + } + } + self.present(alertController, nil) + } + + @discardableResult private func open(url: String, configuration: WKWebViewConfiguration? = nil, new: Bool) -> WKWebView? { + let subject: BrowserScreen.Subject = .webPage(url: url) + if new, let navigationController = self.getNavigationController() { + navigationController._keepModalDismissProgress = true + self.minimize() + let controller = BrowserScreen(context: self.context, subject: subject, preferredConfiguration: configuration, openPreviousOnClose: true) + navigationController._keepModalDismissProgress = true + navigationController.pushViewController(controller) + return (controller.node.content.last as? BrowserWebContent)?.webView + } else { + self.pushContent(subject) + } + return nil + } + + private func share(url: String) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let shareController = ShareController(context: self.context, subject: .url(url)) + shareController.actionCompleted = { [weak self] in + self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + } + self.present(shareController, nil) + } + + private func parseFavicon() { + let addToRecentsWhenReady = self.addToRecentsWhenReady + self.addToRecentsWhenReady = false + + struct Favicon: Equatable, Hashable { + let url: String + let dimensions: PixelDimensions? + + func hash(into hasher: inout Hasher) { + hasher.combine(self.url) + if let dimensions = self.dimensions { + hasher.combine(dimensions.width) + hasher.combine(dimensions.height) + } + } + } + + let js = """ + var favicons = []; + var nodeList = document.getElementsByTagName('link'); + for (var i = 0; i < nodeList.length; i++) + { + if((nodeList[i].getAttribute('rel') == 'icon')||(nodeList[i].getAttribute('rel') == 'shortcut icon')||(nodeList[i].getAttribute('rel').startsWith('apple-touch-icon'))) + { + const node = nodeList[i]; + favicons.push({ + url: node.getAttribute('href'), + sizes: node.getAttribute('sizes') + }); + } + } + favicons; + """ + self.webView.evaluateJavaScript(js, completionHandler: { [weak self] jsResult, _ in + guard let self, let favicons = jsResult as? [Any] else { + return + } + var result = Set(); + for favicon in favicons { + if let faviconDict = favicon as? [String: Any], let urlString = faviconDict["url"] as? String { + if let url = URL(string: urlString, relativeTo: self.webView.url) { + let sizesString = faviconDict["sizes"] as? String; + let sizeStrings = sizesString?.components(separatedBy: "x") ?? [] + if (sizeStrings.count == 2) { + let width = Int(sizeStrings[0]) + let height = Int(sizeStrings[1]) + let dimensions: PixelDimensions? + if let width, let height { + dimensions = PixelDimensions(width: Int32(width), height: Int32(height)) + } else { + dimensions = nil + } + result.insert(Favicon(url: url.absoluteString, dimensions: dimensions)) + } else { + result.insert(Favicon(url: url.absoluteString, dimensions: nil)) + } + } + } + } + + if result.isEmpty, let webViewUrl = self.webView.url { + let schemeAndHostUrl = webViewUrl.deletingPathExtension() + let url = schemeAndHostUrl.appendingPathComponent("favicon.ico") + result.insert(Favicon(url: url.absoluteString, dimensions: nil)) + } + + var largestIcon: Favicon? // = result.first(where: { $0.url.lowercased().contains(".svg") }) + if largestIcon == nil { + largestIcon = result.first + for icon in result { + let maxSize = largestIcon?.dimensions?.width ?? 0 + if let width = icon.dimensions?.width, width > maxSize { + largestIcon = icon + } + } + } + + if let favicon = largestIcon { + self.faviconDisposable.set((fetchFavicon(context: self.context, url: favicon.url, size: CGSize(width: 20.0, height: 20.0)) + |> deliverOnMainQueue).startStrict(next: { [weak self] favicon in + guard let self else { + return + } + self.updateState { $0.withUpdatedFavicon(favicon) } + + if addToRecentsWhenReady { + var image: TelegramMediaImage? + + if let favicon, let imageData = favicon.pngData() { + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: imageData) + image = TelegramMediaImage( + imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), + representations: [ + TelegramMediaImageRepresentation( + dimensions: PixelDimensions(width: Int32(favicon.size.width), height: Int32(favicon.size.height)), + resource: resource, + progressiveSizes: [], + immediateThumbnailData: nil, + hasVideo: false, + isPersonal: false + ) + ], + immediateThumbnailData: nil, + reference: nil, + partialReference: nil, + flags: [] + ) + } + + let webPage = TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent( + url: self._state.url, + displayUrl: self._state.url, + hash: 0, + type: "", + websiteName: self._state.title, + title: self._state.title, + text: nil, + embedUrl: nil, + embedType: nil, + embedSize: nil, + duration: nil, + author: nil, + isMediaLargeByDefault: nil, + image: image, + file: nil, + story: nil, + attributes: [], + instantPage: nil)) + ) + + let _ = addRecentlyVisitedLink(engine: self.context.engine, webPage: webPage).startStandalone() + } + })) + } + }) + } + + private var addToRecentsWhenReady = false + func addToRecentlyVisited() { + self.addToRecentsWhenReady = true + } + + func makeContentSnapshotView() -> UIView? { + 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 + } +} + +private final class ErrorComponent: CombinedComponent { + let theme: PresentationTheme + let title: String + let text: String + let insets: UIEdgeInsets + + init( + theme: PresentationTheme, + title: String, + text: String, + insets: UIEdgeInsets + ) { + self.theme = theme + self.title = title + self.text = text + self.insets = insets + } + + static func ==(lhs: ErrorComponent, rhs: ErrorComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.insets != rhs.insets { + return false + } + return true + } + + static var body: Body { + let background = Child(Rectangle.self) + let animation = Child(LottieComponent.self) + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + + return { context in + var contentHeight: CGFloat = 0.0 + let animationSize = 148.0 + let animationSpacing: CGFloat = 8.0 + let textSpacing: CGFloat = 8.0 + + let constrainedWidth = context.availableSize.width - 76.0 - context.component.insets.left - context.component.insets.right + + let background = background.update( + component: Rectangle(color: context.component.theme.list.plainBackgroundColor), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + let animation = animation.update( + component: LottieComponent( + content: LottieComponent.AppBundleContent(name: "ChatListNoResults") + ), + environment: {}, + availableSize: CGSize(width: animationSize, height: animationSize), + transition: .immediate + ) + contentHeight += animation.size.height + animationSpacing + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: context.component.title, + font: Font.semibold(17.0), + textColor: context.component.theme.list.itemSecondaryTextColor + )), + horizontalAlignment: .center + ), + environment: {}, + availableSize: CGSize(width: constrainedWidth, height: context.availableSize.height), + transition: .immediate + ) + contentHeight += title.size.height + textSpacing + + let text = text.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: context.component.text, + font: Font.regular(15.0), + textColor: context.component.theme.list.itemSecondaryTextColor + )), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + ), + environment: {}, + availableSize: CGSize(width: constrainedWidth, height: context.availableSize.height), + transition: .immediate + ) + contentHeight += text.size.height + + var originY = floor((context.availableSize.height - contentHeight) / 2.0) + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + animation.size.height / 2.0)) + ) + originY += animation.size.height + animationSpacing + + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + title.size.height / 2.0)) + ) + originY += title.size.height + textSpacing + + context.add(text + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + text.size.height / 2.0)) + ) + + return context.availableSize + } + } +} + +let setupFontFunctions = """ +(function() { + const styleId = 'telegram-font-overrides'; + + function setTelegramFontOverrides(font, textSizeAdjust) { + let style = document.getElementById(styleId); + + if (!style) { + style = document.createElement('style'); + style.id = styleId; + document.head.appendChild(style); + } + + let cssRules = '* {'; + if (font !== null) { + cssRules += ` + font-family: ${font} !important; + `; + } + if (textSizeAdjust !== null) { + cssRules += ` + -webkit-text-size-adjust: ${textSizeAdjust} !important; + `; + } + cssRules += '}'; + + style.innerHTML = cssRules; + + if (font === null && textSizeAdjust === null) { + style.parentNode.removeChild(style); + } + } + window.setTelegramFontOverrides = setTelegramFontOverrides; +})(); +""" + +private let videoSource = """ +function disableWebkitEnterFullscreen(videoElement) { + if (videoElement && videoElement.webkitEnterFullscreen) { + Object.defineProperty(videoElement, 'webkitEnterFullscreen', { + value: undefined + }); + } +} + +function disableFullscreenOnExistingVideos() { + document.querySelectorAll('video').forEach(disableWebkitEnterFullscreen); +} + +function handleMutations(mutations) { + mutations.forEach((mutation) => { + if (mutation.addedNodes && mutation.addedNodes.length > 0) { + mutation.addedNodes.forEach((newNode) => { + if (newNode.tagName === 'VIDEO') { + disableWebkitEnterFullscreen(newNode); + } + if (newNode.querySelectorAll) { + newNode.querySelectorAll('video').forEach(disableWebkitEnterFullscreen); + } + }); + } + }); +} + +disableFullscreenOnExistingVideos(); + +const observer = new MutationObserver(handleMutations); + +observer.observe(document.body, { + childList: true, + subtree: true +}); + +function disconnectObserver() { + observer.disconnect(); +} +""" + +let setupTouchObservers = +""" +(function() { + function saveOriginalCssProperties(element) { + while (element) { + const computedStyle = window.getComputedStyle(element); + const propertiesToSave = ['transform', 'top', 'left']; + + element._originalProperties = {}; + + for (const property of propertiesToSave) { + element._originalProperties[property] = computedStyle.getPropertyValue(property); + } + + element = element.parentElement; + } + } + + function checkForCssChanges(element) { + while (element) { + if (!element._originalProperties) return false; + const computedStyle = window.getComputedStyle(element); + const modifiedProperties = ['transform', 'top', 'left']; + + for (const property of modifiedProperties) { + if (computedStyle.getPropertyValue(property) !== element._originalProperties[property]) { + return true; + } + } + + element = element.parentElement; + } + + return false; + } + + function clearOriginalCssProperties(element) { + while (element) { + delete element._originalProperties; + element = element.parentElement; + } + } + + let touchedElement = null; + + document.addEventListener('touchstart', function(event) { + touchedElement = event.target; + saveOriginalCssProperties(touchedElement); + }, { passive: true }); + + document.addEventListener('touchmove', function(event) { + if (checkForCssChanges(touchedElement)) { + TelegramWebviewProxy.postEvent("cancellingTouch", {}) + console.log('CSS properties changed during touchmove'); + } + }, { passive: true }); + + document.addEventListener('touchend', function() { + clearOriginalCssProperties(touchedElement); + touchedElement = null; + }, { passive: true }); +})(); +""" + +private let eventProxySource = "var TelegramWebviewProxyProto = function() {}; " + + "TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " + + "window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " + + "}; " + +"var TelegramWebviewProxy = new TelegramWebviewProxyProto();" + +@available(iOS 16.0, *) +final class BrowserSearchOptions: UITextSearchOptions { + override var wordMatchMethod: UITextSearchOptions.WordMatchMethod { + return .contains + } + + override var stringCompareOptions: NSString.CompareOptions { + return .caseInsensitive + } +} + +private func findScrollView(view: UIView?) -> UIScrollView? { + if let view = view { + if let view = view as? UIScrollView { + return view + } + return findScrollView(view: view.superview) + } else { + return nil } } diff --git a/submodules/BrowserUI/Sources/SectionHeaderComponent.swift b/submodules/BrowserUI/Sources/SectionHeaderComponent.swift new file mode 100644 index 00000000000..294d5c1a34b --- /dev/null +++ b/submodules/BrowserUI/Sources/SectionHeaderComponent.swift @@ -0,0 +1,174 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import MultilineTextComponent + +final class SectionHeaderComponent: Component { + enum Style { + case blocks + case plain + } + let theme: PresentationTheme + let style: Style + let title: String + let insets: UIEdgeInsets + let actionTitle: String? + let action: (() -> Void)? + + init( + theme: PresentationTheme, + style: Style, + title: String, + insets: UIEdgeInsets, + actionTitle: String?, + action: (() -> Void)? + ) { + self.theme = theme + self.style = style + self.title = title + self.insets = insets + self.actionTitle = actionTitle + self.action = action + } + + static func ==(lhs: SectionHeaderComponent, rhs: SectionHeaderComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.style != rhs.style { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.insets != rhs.insets { + return false + } + if lhs.actionTitle != rhs.actionTitle { + return false + } + return true + } + + final class View: UIView { + private let title = ComponentView() + private let backgroundView: BlurredBackgroundView + private let action = ComponentView() + + private var component: SectionHeaderComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: SectionHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + self.state = state + + let height: CGFloat = 28.0 + let leftInset: CGFloat = 16.0 + component.insets.left + let rightInset: CGFloat = 0.0 + + let previousTitleFrame = self.title.view?.frame + + if themeUpdated { + switch component.style { + case .plain: + self.backgroundView.isHidden = false + self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + case .blocks: + self.backgroundView.isHidden = true + } + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + titleView.frame = titleFrame + if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { + transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) + } + } + + if let actionTitle = component.actionTitle { + let actionSize = self.action.update( + transition: .immediate, + component: AnyComponent( + Button(content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: actionTitle, font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), action: { [weak self] in + if let self, let component = self.component { + component.action?() + } + }) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + if let view = self.action.view { + if view.superview == nil { + self.addSubview(view) + if !transition.animation.isImmediate { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + } + } + let actionFrame = CGRect(origin: CGPoint(x: availableSize.width - leftInset - actionSize.width, y: floor((height - titleSize.height) / 2.0)), size: actionSize) + view.frame = actionFrame + } + } else if let view = self.action.view, view.superview != nil { + if !transition.animation.isImmediate { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { finished in + if finished { + view.removeFromSuperview() + view.layer.removeAllAnimations() + } + }) + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } else { + view.removeFromSuperview() + } + } + + let size = CGSize(width: availableSize.width, height: height) + + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundView.update(size: size, transition: transition.containedViewLayoutTransition) + + return size + } + } + + 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) + } +} diff --git a/submodules/BrowserUI/Sources/Utils.swift b/submodules/BrowserUI/Sources/Utils.swift new file mode 100644 index 00000000000..71489f05a37 --- /dev/null +++ b/submodules/BrowserUI/Sources/Utils.swift @@ -0,0 +1,154 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import AccountContext +import TextFormat +import UrlWhitelist +import Svg + +private var faviconCache: [String: UIImage] = [:] +func fetchFavicon(context: AccountContext, url: String, size: CGSize) -> Signal { + if let icon = faviconCache[url] { + return .single(icon) + } + return context.engine.resources.httpData(url: url) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { data in + if let data { + if let image = UIImage(data: data) { + return image + } else if url.lowercased().contains(".svg"), let preparedData = prepareSvgImage(data, false), let image = renderPreparedImage(preparedData, size, .clear, UIScreenScale, false) { + return image + } + return nil + } else { + return nil + } + } + |> beforeNext { image in + if let image { + Queue.mainQueue().async { + faviconCache[url] = image + } + } + } +} + +func getPrimaryUrl(message: Message) -> String? { + var primaryUrl: String? + if let webPage = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, let url = webPage.content.url { + primaryUrl = url + } else { + var entities = message.textEntitiesAttribute?.entities + if entities == nil { + let parsedEntities = generateTextEntities(message.text, enabledTypes: .all) + if !parsedEntities.isEmpty { + entities = parsedEntities + } + } + + if let entities { + loop: for entity in entities { + switch entity.type { + case .Url, .Email: + var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + let nsString = message.text as NSString + if range.location + range.length > nsString.length { + range.location = max(0, nsString.length - range.length) + range.length = nsString.length - range.location + } + let tempUrlString = nsString.substring(with: range) + + var (urlString, concealed) = parseUrl(url: tempUrlString, wasConcealed: false) + var parsedUrl = URL(string: urlString) + if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") { + urlString = "http://" + urlString + parsedUrl = URL(string: urlString) + } + var host: String? = concealed ? urlString : parsedUrl?.host + if host == nil { + host = urlString + } + if let _ = parsedUrl, let _ = host { + primaryUrl = urlString + } + break loop + case let .TextUrl(url): + let messageText = message.text + + var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + let nsString = messageText as NSString + if range.location + range.length > nsString.length { + range.location = max(0, nsString.length - range.length) + range.length = nsString.length - range.location + } + + var (urlString, concealed) = parseUrl(url: url, wasConcealed: false) + var parsedUrl = URL(string: urlString) + if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") { + urlString = "http://" + urlString + parsedUrl = URL(string: urlString) + } + let host: String? = concealed ? urlString : parsedUrl?.host + if let _ = parsedUrl, let _ = host { + primaryUrl = urlString + } + break loop + default: + break + } + } + } + } + return primaryUrl +} + +private let asciiChars = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!) + +func getDisplayUrl(_ url: String, hostOnly: Bool = false, trim: Bool = true) -> String { + if hostOnly { + var title = url + if let parsedUrl = URL(string: url) { + title = parsedUrl.host ?? url + if title.hasPrefix("www.") { + title.removeSubrange(title.startIndex ..< title.index(title.startIndex, offsetBy: 4)) + } + if let decoded = title.idnaDecoded, title != decoded { + if decoded.lowercased().rangeOfCharacter(from: asciiChars) == nil { + title = decoded + } + } + } + return title + } else { + var address = url + if let components = URLComponents(string: address) { + if #available(iOS 16.0, *), let encodedHost = components.encodedHost { + if let decodedHost = components.host, encodedHost != decodedHost { + if decodedHost.lowercased().rangeOfCharacter(from: asciiChars) == nil { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } + } else if let encodedHost = components.host { + if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost { + if decodedHost.lowercased().rangeOfCharacter(from: asciiChars) == nil { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } + } + } + if trim { + address = address.replacingOccurrences(of: "https://www.", with: "") + address = address.replacingOccurrences(of: "https://", with: "") + address = address.replacingOccurrences(of: "tonsite://", with: "") + address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + } + return address + } +} diff --git a/submodules/Camera/BUILD b/submodules/Camera/BUILD index dc8607190e0..f8c62307d75 100644 --- a/submodules/Camera/BUILD +++ b/submodules/Camera/BUILD @@ -62,6 +62,7 @@ swift_library( "//submodules/Display:Display", "//submodules/ImageBlur:ImageBlur", "//submodules/TelegramCore:TelegramCore", + "//submodules/Utils/DeviceModel", ], visibility = [ "//visibility:public", diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index d1f95d0bc99..ca84dca958f 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -4,6 +4,7 @@ import SwiftSignalKit import AVFoundation import CoreImage import TelegramCore +import DeviceModel final class CameraSession { private let singleSession: AVCaptureSession? @@ -559,7 +560,11 @@ private final class CameraContext { guard let mainDeviceContext = self.mainDeviceContext else { return .complete() } - mainDeviceContext.device.setTorchMode(self._flashMode) + if self.initialConfiguration.isRoundVideo && self.positionValue == .front { + + } else { + mainDeviceContext.device.setTorchMode(self._flashMode) + } let orientation = self.simplePreviewView?.videoPreviewLayer.connection?.videoOrientation ?? .portrait if self.initialConfiguration.isRoundVideo { diff --git a/submodules/Camera/Sources/CameraMetrics.swift b/submodules/Camera/Sources/CameraMetrics.swift index c1c8a3e4296..4d5c684da88 100644 --- a/submodules/Camera/Sources/CameraMetrics.swift +++ b/submodules/Camera/Sources/CameraMetrics.swift @@ -1,4 +1,5 @@ import Foundation +import DeviceModel public extension Camera { enum Metrics { @@ -56,370 +57,3 @@ public extension Camera { } } } - -enum DeviceModel: CaseIterable, Equatable { - static var allCases: [DeviceModel] { - return [ - .iPodTouch1, - .iPodTouch2, - .iPodTouch3, - .iPodTouch4, - .iPodTouch5, - .iPodTouch6, - .iPodTouch7, - .iPhone, - .iPhone3G, - .iPhone3GS, - .iPhone4, - .iPhone4S, - .iPhone5, - .iPhone5C, - .iPhone5S, - .iPhone6, - .iPhone6Plus, - .iPhone6S, - .iPhone6SPlus, - .iPhoneSE, - .iPhone7, - .iPhone7Plus, - .iPhone8, - .iPhone8Plus, - .iPhoneX, - .iPhoneXS, - .iPhoneXR, - .iPhone11, - .iPhone11Pro, - .iPhone11ProMax, - .iPhone12, - .iPhone12Mini, - .iPhone12Pro, - .iPhone12ProMax, - .iPhone13, - .iPhone13Mini, - .iPhone13Pro, - .iPhone13ProMax, - .iPhone14, - .iPhone14Plus, - .iPhone14Pro, - .iPhone14ProMax, - .iPhone15, - .iPhone15Plus, - .iPhone15Pro, - .iPhone15ProMax - ] - } - - case iPodTouch1 - case iPodTouch2 - case iPodTouch3 - case iPodTouch4 - case iPodTouch5 - case iPodTouch6 - case iPodTouch7 - - case iPhone - case iPhone3G - case iPhone3GS - - case iPhone4 - case iPhone4S - - case iPhone5 - case iPhone5C - case iPhone5S - - case iPhone6 - case iPhone6Plus - case iPhone6S - case iPhone6SPlus - - case iPhoneSE - - case iPhone7 - case iPhone7Plus - case iPhone8 - case iPhone8Plus - - case iPhoneX - case iPhoneXS - case iPhoneXSMax - case iPhoneXR - - case iPhone11 - case iPhone11Pro - case iPhone11ProMax - - case iPhoneSE2ndGen - - case iPhone12 - case iPhone12Mini - case iPhone12Pro - case iPhone12ProMax - - case iPhone13 - case iPhone13Mini - case iPhone13Pro - case iPhone13ProMax - - case iPhoneSE3rdGen - - case iPhone14 - case iPhone14Plus - case iPhone14Pro - case iPhone14ProMax - - case iPhone15 - case iPhone15Plus - case iPhone15Pro - case iPhone15ProMax - - case unknown(String) - - var modelId: [String] { - switch self { - case .iPodTouch1: - return ["iPod1,1"] - case .iPodTouch2: - return ["iPod2,1"] - case .iPodTouch3: - return ["iPod3,1"] - case .iPodTouch4: - return ["iPod4,1"] - case .iPodTouch5: - return ["iPod5,1"] - case .iPodTouch6: - return ["iPod7,1"] - case .iPodTouch7: - return ["iPod9,1"] - case .iPhone: - return ["iPhone1,1"] - case .iPhone3G: - return ["iPhone1,2"] - case .iPhone3GS: - return ["iPhone2,1"] - case .iPhone4: - return ["iPhone3,1", "iPhone3,2", "iPhone3,3"] - case .iPhone4S: - return ["iPhone4,1", "iPhone4,2", "iPhone4,3"] - case .iPhone5: - return ["iPhone5,1", "iPhone5,2"] - case .iPhone5C: - return ["iPhone5,3", "iPhone5,4"] - case .iPhone5S: - return ["iPhone6,1", "iPhone6,2"] - case .iPhone6: - return ["iPhone7,2"] - case .iPhone6Plus: - return ["iPhone7,1"] - case .iPhone6S: - return ["iPhone8,1"] - case .iPhone6SPlus: - return ["iPhone8,2"] - case .iPhoneSE: - return ["iPhone8,4"] - case .iPhone7: - return ["iPhone9,1", "iPhone9,3"] - case .iPhone7Plus: - return ["iPhone9,2", "iPhone9,4"] - case .iPhone8: - return ["iPhone10,1", "iPhone10,4"] - case .iPhone8Plus: - return ["iPhone10,2", "iPhone10,5"] - case .iPhoneX: - return ["iPhone10,3", "iPhone10,6"] - case .iPhoneXS: - return ["iPhone11,2"] - case .iPhoneXSMax: - return ["iPhone11,4", "iPhone11,6"] - case .iPhoneXR: - return ["iPhone11,8"] - case .iPhone11: - return ["iPhone12,1"] - case .iPhone11Pro: - return ["iPhone12,3"] - case .iPhone11ProMax: - return ["iPhone12,5"] - case .iPhoneSE2ndGen: - return ["iPhone12,8"] - case .iPhone12: - return ["iPhone13,2"] - case .iPhone12Mini: - return ["iPhone13,1"] - case .iPhone12Pro: - return ["iPhone13,3"] - case .iPhone12ProMax: - return ["iPhone13,4"] - case .iPhone13: - return ["iPhone14,5"] - case .iPhone13Mini: - return ["iPhone14,4"] - case .iPhone13Pro: - return ["iPhone14,2"] - case .iPhone13ProMax: - return ["iPhone14,3"] - case .iPhoneSE3rdGen: - return ["iPhone14,6"] - case .iPhone14: - return ["iPhone14,7"] - case .iPhone14Plus: - return ["iPhone14,8"] - case .iPhone14Pro: - return ["iPhone15,2"] - case .iPhone14ProMax: - return ["iPhone15,3"] - case .iPhone15: - return ["iPhone15,4"] - case .iPhone15Plus: - return ["iPhone15,5"] - case .iPhone15Pro: - return ["iPhone16,1"] - case .iPhone15ProMax: - return ["iPhone16,2"] - case let .unknown(modelId): - return [modelId] - } - } - - var modelName: String { - switch self { - case .iPodTouch1: - return "iPod touch 1G" - case .iPodTouch2: - return "iPod touch 2G" - case .iPodTouch3: - return "iPod touch 3G" - case .iPodTouch4: - return "iPod touch 4G" - case .iPodTouch5: - return "iPod touch 5G" - case .iPodTouch6: - return "iPod touch 6G" - case .iPodTouch7: - return "iPod touch 7G" - case .iPhone: - return "iPhone" - case .iPhone3G: - return "iPhone 3G" - case .iPhone3GS: - return "iPhone 3GS" - case .iPhone4: - return "iPhone 4" - case .iPhone4S: - return "iPhone 4S" - case .iPhone5: - return "iPhone 5" - case .iPhone5C: - return "iPhone 5C" - case .iPhone5S: - return "iPhone 5S" - case .iPhone6: - return "iPhone 6" - case .iPhone6Plus: - return "iPhone 6 Plus" - case .iPhone6S: - return "iPhone 6S" - case .iPhone6SPlus: - return "iPhone 6S Plus" - case .iPhoneSE: - return "iPhone SE" - case .iPhone7: - return "iPhone 7" - case .iPhone7Plus: - return "iPhone 7 Plus" - case .iPhone8: - return "iPhone 8" - case .iPhone8Plus: - return "iPhone 8 Plus" - case .iPhoneX: - return "iPhone X" - case .iPhoneXS: - return "iPhone XS" - case .iPhoneXSMax: - return "iPhone XS Max" - case .iPhoneXR: - return "iPhone XR" - case .iPhone11: - return "iPhone 11" - case .iPhone11Pro: - return "iPhone 11 Pro" - case .iPhone11ProMax: - return "iPhone 11 Pro Max" - case .iPhoneSE2ndGen: - return "iPhone SE (2nd gen)" - case .iPhone12: - return "iPhone 12" - case .iPhone12Mini: - return "iPhone 12 mini" - case .iPhone12Pro: - return "iPhone 12 Pro" - case .iPhone12ProMax: - return "iPhone 12 Pro Max" - case .iPhone13: - return "iPhone 13" - case .iPhone13Mini: - return "iPhone 13 mini" - case .iPhone13Pro: - return "iPhone 13 Pro" - case .iPhone13ProMax: - return "iPhone 13 Pro Max" - case .iPhoneSE3rdGen: - return "iPhone SE (3rd gen)" - case .iPhone14: - return "iPhone 14" - case .iPhone14Plus: - return "iPhone 14 Plus" - case .iPhone14Pro: - return "iPhone 14 Pro" - case .iPhone14ProMax: - return "iPhone 14 Pro Max" - case .iPhone15: - return "iPhone 15" - case .iPhone15Plus: - return "iPhone 15 Plus" - case .iPhone15Pro: - return "iPhone 15 Pro" - case .iPhone15ProMax: - return "iPhone 15 Pro Max" - case let .unknown(modelId): - if modelId.hasPrefix("iPhone") { - return "Unknown iPhone" - } else if modelId.hasPrefix("iPod") { - return "Unknown iPod" - } else if modelId.hasPrefix("iPad") { - return "Unknown iPad" - } else { - return "Unknown Device" - } - } - } - - var isIpad: Bool { - return self.modelId.first?.hasPrefix("iPad") ?? false - } - - static let current = DeviceModel() - - private init() { - var systemInfo = utsname() - uname(&systemInfo) - let modelCode = withUnsafePointer(to: &systemInfo.machine) { - $0.withMemoryRebound(to: CChar.self, capacity: 1) { - ptr in String.init(validatingUTF8: ptr) - } - } - var result: DeviceModel? - if let modelCode { - for model in DeviceModel.allCases { - if model.modelId.contains(modelCode) { - result = model - break - } - } - } - if let result { - self = result - } else { - self = .unknown(modelCode ?? "") - } - } -} diff --git a/submodules/Camera/Sources/VideoRecorder.swift b/submodules/Camera/Sources/VideoRecorder.swift index e785db18348..09dd80a0396 100644 --- a/submodules/Camera/Sources/VideoRecorder.swift +++ b/submodules/Camera/Sources/VideoRecorder.swift @@ -204,9 +204,9 @@ private final class VideoRecorderImpl { if let videoInput = self.videoInput { let time = CACurrentMediaTime() - if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime { - print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)") - } +// if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime { +// print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)") +// } self.previousPresentationTime = presentationTime.seconds self.previousAppendTime = time diff --git a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift index 0442a760a1a..68e3aa5abd8 100644 --- a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift +++ b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift @@ -460,7 +460,7 @@ public final class ChatImportActivityScreen: ViewController { if let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) { let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black) - let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [], preloadSize: nil)]) + let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [], preloadSize: nil, coverTime: nil)]) let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil) diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index c45ed74a81b..92a2941beb2 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -145,319 +145,331 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch }))) items.append(.separator) } - } - } - - let isSavedMessages = peerId == context.account.peerId - - if !isSavedMessages, case let .user(peer) = peer, !peer.flags.contains(.isSupport), peer.botInfo == nil && !peer.isDeleted { - if !isContact { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToContacts, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) }, action: { _, f in - context.sharedContext.openAddPersonContact(context: context, peerId: peerId, pushController: { controller in - if let navigationController = chatListController?.navigationController as? NavigationController { - navigationController.pushViewController(controller) - } - }, present: { c, a in - if let chatListController = chatListController { - chatListController.present(c, in: .window(.root), with: a) - } + case .recentApps: + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromRecents, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor) }, action: { _, f in + let _ = (context.engine.peers.removeRecentlyUsedApp(peerId: peerId) + |> deliverOnMainQueue).startStandalone(completed: { + f(.default) }) - f(.default) }))) items.append(.separator) + case .popularApps: + break } } - var isMuted = false - if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { - isMuted = true - } else if case .default = notificationSettings.muteState { - if case .user = peer { - isMuted = !globalNotificationSettings.privateChats.enabled - } else if case .legacyGroup = peer { - isMuted = !globalNotificationSettings.groupChats.enabled - } else if case let .channel(channel) = peer { - switch channel.info { - case .group: + if case .search(.recentApps) = source { + } else if case .search(.popularApps) = source { + } else { + let isSavedMessages = peerId == context.account.peerId + + if !isSavedMessages, case let .user(peer) = peer, !peer.flags.contains(.isSupport), peer.botInfo == nil && !peer.isDeleted { + if !isContact { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToContacts, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) }, action: { _, f in + context.sharedContext.openAddPersonContact(context: context, peerId: peerId, pushController: { controller in + if let navigationController = chatListController?.navigationController as? NavigationController { + navigationController.pushViewController(controller) + } + }, present: { c, a in + if let chatListController = chatListController { + chatListController.present(c, in: .window(.root), with: a) + } + }) + f(.default) + }))) + items.append(.separator) + } + } + + var isMuted = false + if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + isMuted = true + } else if case .default = notificationSettings.muteState { + if case .user = peer { + isMuted = !globalNotificationSettings.privateChats.enabled + } else if case .legacyGroup = peer { isMuted = !globalNotificationSettings.groupChats.enabled - case .broadcast: - isMuted = !globalNotificationSettings.channels.enabled + } else if case let .channel(channel) = peer { + switch channel.info { + case .group: + isMuted = !globalNotificationSettings.groupChats.enabled + case .broadcast: + isMuted = !globalNotificationSettings.channels.enabled + } } } - } - - var isUnread = false - if readCounters.isUnread { - isUnread = true - } - - var isForum = false - if case let .channel(channel) = peer, channel.flags.contains(.isForum) { - isForum = true - } - - var hasRemoveFromFolder = false - if case let .chatList(currentFilter) = source { - if let currentFilter = currentFilter, case let .filter(id, title, emoticon, data) = currentFilter { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/RemoveFromFolder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in - let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in - var filters = filters - for i in 0 ..< filters.count { - if filters[i].id == currentFilter.id { - var updatedData = data - let _ = updatedData.addExcludePeer(peerId: peer.id) - filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) - break + + var isUnread = false + if readCounters.isUnread { + isUnread = true + } + + var isForum = false + if case let .channel(channel) = peer, channel.flags.contains(.isForum) { + isForum = true + } + + var hasRemoveFromFolder = false + if case let .chatList(currentFilter) = source { + if let currentFilter = currentFilter, case let .filter(id, title, emoticon, data) = currentFilter { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/RemoveFromFolder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in + var filters = filters + for i in 0 ..< filters.count { + if filters[i].id == currentFilter.id { + var updatedData = data + let _ = updatedData.addExcludePeer(peerId: peer.id) + filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) + break + } } + return filters } - return filters - } - |> deliverOnMainQueue).startStandalone(completed: { - c?.dismiss(completion: { - chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in - return false - }), in: .current) + |> deliverOnMainQueue).startStandalone(completed: { + c?.dismiss(completion: { + chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + return false + }), in: .current) + }) }) - }) - }))) - hasRemoveFromFolder = true + }))) + hasRemoveFromFolder = true + } } - } - - if !hasRemoveFromFolder && peerGroup != nil { - var hasFolders = false - - for case let .filter(_, _, _, data) in filters { - let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId) - if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { - continue + + if !hasRemoveFromFolder && peerGroup != nil { + var hasFolders = false + + for case let .filter(_, _, _, data) in filters { + let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId) + if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { + continue + } + + var data = data + if data.addIncludePeer(peerId: peer.id) { + hasFolders = true + break + } } - - var data = data - if data.addIncludePeer(peerId: peer.id) { - hasFolders = true - break + + if hasFolders { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in + var updatedItems: [ContextMenuItem] = [] + + for filter in filters { + if case let .filter(_, title, _, data) = filter { + let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId) + if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { + continue + } + + var data = data + if !data.addIncludePeer(peerId: peer.id) { + continue + } + + let filterType = chatListFilterType(data) + updatedItems.append(.action(ContextMenuActionItem(text: title, icon: { theme in + let imageName: String + switch filterType { + case .generic: + imageName = "Chat/Context Menu/List" + case .unmuted: + imageName = "Chat/Context Menu/Unmute" + case .unread: + imageName = "Chat/Context Menu/MarkAsUnread" + case .channels: + imageName = "Chat/Context Menu/Channels" + case .groups: + imageName = "Chat/Context Menu/Groups" + case .bots: + imageName = "Chat/Context Menu/Bots" + case .contacts: + imageName = "Chat/Context Menu/User" + case .nonContacts: + imageName = "Chat/Context Menu/UnknownUser" + } + return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor) + }, action: { c, f in + c?.dismiss(completion: { + let isPremium = limitsData.0?.isPremium ?? false + let (_, limits, premiumLimits) = limitsData + + let limit = limits.maxFolderChatsCount + let premiumLimit = premiumLimits.maxFolderChatsCount + + let count = data.includePeers.peers.count - 1 + if count >= premiumLimit { + let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { + return true + }) + chatListController?.push(controller) + return + } else if count >= limit && !isPremium { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { + let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder) + replaceImpl?(controller) + return true + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + chatListController?.push(controller) + return + } + + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in + var filters = filters + for i in 0 ..< filters.count { + if filters[i].id == filter.id { + if case let .filter(id, title, emoticon, data) = filter { + var updatedData = data + let _ = updatedData.addIncludePeer(peerId: peer.id) + filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) + } + break + } + } + return filters + }).startStandalone() + + chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + return false + }), in: .current) + }) + }))) + } + } + + updatedItems.append(.separator) + updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { c, _ in + c?.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) + }))) + + c?.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil, animated: true) + }))) } } - - if hasFolders { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in - var updatedItems: [ContextMenuItem] = [] - - for filter in filters { - if case let .filter(_, title, _, data) = filter { - let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId) - if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { - continue - } - - var data = data - if !data.addIncludePeer(peerId: peer.id) { - continue + + if isUnread { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsRead, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsRead"), color: theme.contextMenu.primaryColor) }, action: { _, f in + let _ = context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil).startStandalone() + f(.default) + }))) + } else if !isForum { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsUnread, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsUnread"), color: theme.contextMenu.primaryColor) }, action: { _, f in + let _ = context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil).startStandalone() + f(.default) + }))) + } + + let archiveEnabled = !isSavedMessages && peerId != PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(777000)) && peerId == context.account.peerId + if let group = peerGroup { + if archiveEnabled { + let isArchived = group == .archive + items.append(.action(ContextMenuActionItem(text: isArchived ? strings.ChatList_Context_Unarchive : strings.ChatList_Context_Archive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isArchived ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) }, action: { _, f in + if isArchived { + let _ = (context.engine.peers.updatePeersGroupIdInteractively(peerIds: [peerId], groupId: .root) + |> deliverOnMainQueue).startStandalone(completed: { + f(.default) + }) + } else { + if let chatListController = chatListController { + chatListController.archiveChats(peerIds: [peerId]) + f(.default) + } else { + let _ = (context.engine.peers.updatePeersGroupIdInteractively(peerIds: [peerId], groupId: .archive) + |> deliverOnMainQueue).startStandalone(completed: { + f(.default) + }) } - - let filterType = chatListFilterType(data) - updatedItems.append(.action(ContextMenuActionItem(text: title, icon: { theme in - let imageName: String - switch filterType { - case .generic: - imageName = "Chat/Context Menu/List" - case .unmuted: - imageName = "Chat/Context Menu/Unmute" - case .unread: - imageName = "Chat/Context Menu/MarkAsUnread" - case .channels: - imageName = "Chat/Context Menu/Channels" - case .groups: - imageName = "Chat/Context Menu/Groups" - case .bots: - imageName = "Chat/Context Menu/Bots" - case .contacts: - imageName = "Chat/Context Menu/User" - case .nonContacts: - imageName = "Chat/Context Menu/UnknownUser" - } - return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor) - }, action: { c, f in - c?.dismiss(completion: { - let isPremium = limitsData.0?.isPremium ?? false - let (_, limits, premiumLimits) = limitsData - - let limit = limits.maxFolderChatsCount - let premiumLimit = premiumLimits.maxFolderChatsCount - - let count = data.includePeers.peers.count - 1 - if count >= premiumLimit { + } + }))) + } + + if isPinned || chatListFilter == nil || peerId.namespace != Namespaces.Peer.SecretChat { + items.append(.action(ContextMenuActionItem(text: isPinned ? strings.ChatList_Context_Unpin : strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { c, f in + let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: .peer(peerId)) + |> deliverOnMainQueue).startStandalone(next: { result in + switch result { + case .done: + f(.default) + case let .limitExceeded(count, _): + f(.default) + + let isPremium = limitsData.0?.isPremium ?? false + if isPremium { + if case .filter = location { let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { return true }) chatListController?.push(controller) - return - } else if count >= limit && !isPremium { + } else { + let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { + return true + }) + chatListController?.push(controller) + } + } else { + if case .filter = location { var replaceImpl: ((ViewController) -> Void)? let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { - let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder) - replaceImpl?(controller) + let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) + replaceImpl?(premiumScreen) return true }) + chatListController?.push(controller) replaceImpl = { [weak controller] c in controller?.replace(with: c) } + } else { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { + let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) + replaceImpl?(premiumScreen) + return true + }) chatListController?.push(controller) - return - } - - let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in - var filters = filters - for i in 0 ..< filters.count { - if filters[i].id == filter.id { - if case let .filter(id, title, emoticon, data) = filter { - var updatedData = data - let _ = updatedData.addIncludePeer(peerId: peer.id) - filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) - } - break - } + replaceImpl = { [weak controller] c in + controller?.replace(with: c) } - return filters - }).startStandalone() - - chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in - return false - }), in: .current) - }) - }))) - } - } - - updatedItems.append(.separator) - updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) - }, iconPosition: .left, action: { c, _ in - c?.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) - }))) - - c?.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil, animated: true) - }))) - } - } - - if isUnread { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsRead, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsRead"), color: theme.contextMenu.primaryColor) }, action: { _, f in - let _ = context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil).startStandalone() - f(.default) - }))) - } else if !isForum { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsUnread, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsUnread"), color: theme.contextMenu.primaryColor) }, action: { _, f in - let _ = context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil).startStandalone() - f(.default) - }))) - } - - let archiveEnabled = !isSavedMessages && peerId != PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(777000)) && peerId == context.account.peerId - if let group = peerGroup { - if archiveEnabled { - let isArchived = group == .archive - items.append(.action(ContextMenuActionItem(text: isArchived ? strings.ChatList_Context_Unarchive : strings.ChatList_Context_Archive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isArchived ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) }, action: { _, f in - if isArchived { - let _ = (context.engine.peers.updatePeersGroupIdInteractively(peerIds: [peerId], groupId: .root) - |> deliverOnMainQueue).startStandalone(completed: { - f(.default) - }) - } else { - if let chatListController = chatListController { - chatListController.archiveChats(peerIds: [peerId]) - f(.default) - } else { - let _ = (context.engine.peers.updatePeersGroupIdInteractively(peerIds: [peerId], groupId: .archive) - |> deliverOnMainQueue).startStandalone(completed: { - f(.default) - }) - } - } - }))) - } - - if isPinned || chatListFilter == nil || peerId.namespace != Namespaces.Peer.SecretChat { - items.append(.action(ContextMenuActionItem(text: isPinned ? strings.ChatList_Context_Unpin : strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { c, f in - let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: .peer(peerId)) - |> deliverOnMainQueue).startStandalone(next: { result in - switch result { - case .done: - f(.default) - case let .limitExceeded(count, _): - f(.default) - - let isPremium = limitsData.0?.isPremium ?? false - if isPremium { - if case .filter = location { - let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { - return true - }) - chatListController?.push(controller) - } else { - let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { - return true - }) - chatListController?.push(controller) - } - } else { - if case .filter = location { - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { - let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) - replaceImpl?(premiumScreen) - return true - }) - chatListController?.push(controller) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - } else { - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { - let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) - replaceImpl?(premiumScreen) - return true - }) - chatListController?.push(controller) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) } } } - } - }) - }))) - } - - if !isSavedMessages { - var isMuted = false - if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { - isMuted = true - } else if case .default = notificationSettings.muteState { - if case .user = peer { - isMuted = !globalNotificationSettings.privateChats.enabled - } else if case .legacyGroup = peer { - isMuted = !globalNotificationSettings.groupChats.enabled - } else if case let .channel(channel) = peer { - switch channel.info { - case .group: + }) + }))) + } + + if !isSavedMessages { + var isMuted = false + if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + isMuted = true + } else if case .default = notificationSettings.muteState { + if case .user = peer { + isMuted = !globalNotificationSettings.privateChats.enabled + } else if case .legacyGroup = peer { isMuted = !globalNotificationSettings.groupChats.enabled - case .broadcast: - isMuted = !globalNotificationSettings.channels.enabled + } else if case let .channel(channel) = peer { + switch channel.info { + case .group: + isMuted = !globalNotificationSettings.groupChats.enabled + case .broadcast: + isMuted = !globalNotificationSettings.channels.enabled + } } } - } - items.append(.action(ContextMenuActionItem(text: isMuted ? strings.ChatList_Context_Unmute : strings.ChatList_Context_Mute, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { _, f in - let _ = (context.engine.peers.togglePeerMuted(peerId: peerId, threadId: nil) - |> deliverOnMainQueue).startStandalone(completed: { - f(.default) - }) - }))) - - // MARK: Nicegram HiddenChats - if #available(iOS 13.0, *) { + items.append(.action(ContextMenuActionItem(text: isMuted ? strings.ChatList_Context_Unmute : strings.ChatList_Context_Mute, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { _, f in + let _ = (context.engine.peers.togglePeerMuted(peerId: peerId, threadId: nil) + |> deliverOnMainQueue).startStandalone(completed: { + f(.default) + }) + }))) + + // MARK: Nicegram HiddenChats let hiddenChatsContainer = HiddenChatsContainer.shared let getChatStatusUseCase = hiddenChatsContainer.getChatStatusUseCase() @@ -520,79 +532,79 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch ) items.append(.action(item)) } + // } - // - } - } else { - if case .search = source { - if case let .channel(peer) = peer { - let text: String - if case .broadcast = peer.info { - text = strings.ChatList_Context_JoinChannel - } else { - text = strings.ChatList_Context_JoinChat - } - items.append(.action(ContextMenuActionItem(text: text, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { _, f in - var createSignal = context.peerChannelMemberCategoriesContextsManager.join(engine: context.engine, peerId: peerId, hash: nil) - var cancelImpl: (() -> Void)? - let progressSignal = Signal { subscriber in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - chatListController?.present(controller, in: .window(.root)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - createSignal = createSignal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - let joinChannelDisposable = MetaDisposable() - cancelImpl = { - joinChannelDisposable.set(nil) + } else { + if case .search = source { + if case let .channel(peer) = peer { + let text: String + if case .broadcast = peer.info { + text = strings.ChatList_Context_JoinChannel + } else { + text = strings.ChatList_Context_JoinChat } - - joinChannelDisposable.set((createSignal - |> deliverOnMainQueue).start(next: { _ in - }, error: { _ in - if let chatListController = chatListController { + items.append(.action(ContextMenuActionItem(text: text, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { _, f in + var createSignal = context.peerChannelMemberCategoriesContextsManager.join(engine: context.engine, peerId: peerId, hash: nil) + var cancelImpl: (() -> Void)? + let progressSignal = Signal { subscriber in let presentationData = context.sharedContext.currentPresentationData.with { $0 } - chatListController.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + chatListController?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } } - }, completed: { - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> deliverOnMainQueue).startStandalone(next: { peer in - guard let peer = peer else { - return + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + createSignal = createSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() } - if let navigationController = (chatListController?.navigationController as? NavigationController) { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) + } + let joinChannelDisposable = MetaDisposable() + cancelImpl = { + joinChannelDisposable.set(nil) + } + + joinChannelDisposable.set((createSignal + |> deliverOnMainQueue).start(next: { _ in + }, error: { _ in + if let chatListController = chatListController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + chatListController.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } - }) - })) - f(.default) - }))) + }, completed: { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).startStandalone(next: { peer in + guard let peer = peer else { + return + } + if let navigationController = (chatListController?.navigationController as? NavigationController) { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) + } + }) + })) + f(.default) + }))) + } } } - } - - if case .chatList = source, peerGroup != nil { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in - if let chatListController = chatListController { - chatListController.deletePeerChat(peerId: peerId, joined: joined) - } - f(.default) - }))) + + if case .chatList = source, peerGroup != nil { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in + if let chatListController = chatListController { + chatListController.deletePeerChat(peerId: peerId, joined: joined) + } + f(.default) + }))) + } } if let item = items.last, case .separator = item { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index b64c4c978d6..5bd1d40f436 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1263,7 +1263,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if case let .channel(channel) = actualPeer, channel.flags.contains(.isForum), let threadId { let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: peer.id, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .never).startStandalone() } else { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), purposefulAction: { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), purposefulAction: { if deactivateOnAction { self?.deactivateSearch(animated: false) } @@ -1524,7 +1524,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } else { var subject: ChatControllerSubject? if case let .search(messageId) = source, let id = messageId { - subject = .message(id: .id(id), highlight: nil, timecode: nil) + subject = .message(id: .id(id), highlight: nil, timecode: nil, setupReply: false) } let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.id), subject: subject, botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) @@ -2892,15 +2892,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return nil } if let componentView = self.chatListHeaderView() { - let peerId: EnginePeer.Id + let peerId: EnginePeer.Id? switch target { case .myStories: peerId = self.context.account.peerId case let .peer(id): peerId = id + case .botPreview: + peerId = nil } - if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) { + if let peerId, let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) { return StoryCameraTransitionOut( destinationView: transitionView, destinationRect: transitionView.bounds, @@ -4117,6 +4119,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController StoryContainerScreen.openPeerStoriesCustom( context: self.context, peerId: peerId, + focusOnId: storyId, isHidden: false, singlePeer: true, parentController: self, @@ -4930,8 +4933,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] if havePrivateChats { - //TODO:localize - items.append(ActionSheetButtonItem(title: haveNonPrivateChats ? "Delete from both sides where possible" : "Delete from both sides", color: .destructive, action: { [weak self, weak actionSheet] in + items.append(ActionSheetButtonItem(title: haveNonPrivateChats ? self.presentationData.strings.ChatList_DeleteForAllWhenPossible : self.presentationData.strings.ChatList_DeleteForAll, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() guard let strongSelf = self else { @@ -5003,8 +5005,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.donePressed() })) } - //TODO:localize - items.append(ActionSheetButtonItem(title: havePrivateChats ? "Delete for me" : self.presentationData.strings.ChatList_DeleteConfirmation(Int32(peerIds.count)), color: .destructive, action: { [weak self, weak actionSheet] in + items.append(ActionSheetButtonItem(title: havePrivateChats ? self.presentationData.strings.ChatList_DeleteForMe : self.presentationData.strings.ChatList_DeleteConfirmation(Int32(peerIds.count)), color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() guard let strongSelf = self else { @@ -6160,15 +6161,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return nil } if let componentView = self.chatListHeaderView() { - let peerId: EnginePeer.Id + let peerId: EnginePeer.Id? switch target { case .myStories: peerId = self.context.account.peerId case let .peer(id): peerId = id + case .botPreview: + peerId = nil } - if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) { + if let peerId, let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) { return StoryCameraTransitionOut( destinationView: transitionView, destinationRect: transitionView.bounds, diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index fbb33bdad33..0658cc82478 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -94,7 +94,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private let context: AccountContext private let peersFilter: ChatListNodePeersFilter private let requestPeerType: [ReplyMarkupButtonRequestPeerType]? - private let location: ChatListControllerLocation + private var location: ChatListControllerLocation private let displaySearchFilters: Bool private let hasDownloads: Bool private var interaction: ChatListSearchInteraction? @@ -148,6 +148,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private var validLayout: (ContainerViewLayout, CGFloat)? private let sharedOpenStoryDisposable = MetaDisposable() + private var recentAppsDisposable: Disposable? public init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?, parentController: @escaping () -> ViewController?) { var initialFilter = initialFilter @@ -175,7 +176,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) self.filterContainerNode = ChatListSearchFiltersContainerNode() - self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, requestPeerType: self.requestPeerType, location: location, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController) + self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, requestPeerType: self.requestPeerType, location: location, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController, parentController: parentController()) self.paneContainerNode.clipsToBounds = true super.init() @@ -296,6 +297,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo filterKey = .topics case .channels: filterKey = .channels + case .apps: + filterKey = .apps case .media: filterKey = .media case .downloads: @@ -330,6 +333,10 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } } + self.paneContainerNode.requesDismissInput = { + parentController()?.view.endEditing(true) + } + self.filterContainerNode.filterPressed = { [weak self] filter in guard let strongSelf = self else { return @@ -350,6 +357,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo key = .topics case .channels: key = .channels + case .apps: + key = .apps case .media: key = .media case .downloads: @@ -389,7 +398,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo switch filter { case let .filter(filter): switch filter { - case .downloads, .channels: + case .downloads, .channels, .apps: return false default: return true @@ -521,6 +530,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo }) } + self.recentAppsDisposable = context.engine.peers.managedUpdatedRecentApps().startStrict() + self._ready.set(self.paneContainerNode.isReady.get() |> map { _ in Void() }) } @@ -531,6 +542,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.suggestedFiltersDisposable.dispose() self.shareStatusDisposable?.dispose() self.sharedOpenStoryDisposable.dispose() + self.recentAppsDisposable?.dispose() self.copyProtectionTooltipController?.dismiss() } @@ -572,6 +584,12 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo for token in tokens { tokensIdSet.insert(token.id) } + + if case .chatList(.archive) = self.location, !tokens.contains(where: { $0.id == AnyHashable(ChatListTokenId.archive.rawValue) }) { + self.location = .chatList(groupId: .root) + self.paneContainerNode.location = self.location + } + if !tokensIdSet.contains(ChatListTokenId.date.rawValue) && updatedOptions?.date != nil { updatedOptions = updatedOptions?.withUpdatedDate(nil) } @@ -585,7 +603,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo var options = options var tokens: [SearchBarToken] = [] if case .chatList(.archive) = self.location { - tokens.append(SearchBarToken(id: ChatListTokenId.archive.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/Archive"), iconOffset: -1.0, title: self.presentationData.strings.ChatList_Archive, permanent: true)) + tokens.append(SearchBarToken(id: ChatListTokenId.archive.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/Archive"), iconOffset: -1.0, title: self.presentationData.strings.ChatList_Archive, permanent: false)) } else if case .forum = self.location, let forumPeer = self.forumPeer { tokens.append(SearchBarToken(id: ChatListTokenId.forum.rawValue, icon: nil, iconOffset: -1.0, peer: (forumPeer, self.context, self.presentationData.theme), title: self.presentationData.strings.ChatList_Archive, permanent: true)) } @@ -665,7 +683,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo var tokens: [SearchBarToken] = [] if case .chatList(.archive) = self.location { - tokens.append(SearchBarToken(id: ChatListTokenId.archive.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/Archive"), iconOffset: -1.0, title: self.presentationData.strings.ChatList_Archive, permanent: true)) + tokens.append(SearchBarToken(id: ChatListTokenId.archive.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/Archive"), iconOffset: -1.0, title: self.presentationData.strings.ChatList_Archive, permanent: false)) } else if case .forum = self.location, let forumPeer = self.forumPeer { tokens.append(SearchBarToken(id: ChatListTokenId.forum.rawValue, icon: nil, iconOffset: -1.0, peer: (forumPeer, self.context, self.presentationData.theme), title: self.presentationData.strings.ChatList_Archive, permanent: true)) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index 5a495927398..127b853313e 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -87,6 +87,9 @@ private final class ItemNode: ASDisplayNode { case .channels: title = presentationData.strings.ChatList_Search_FilterChannels icon = nil + case .apps: + title = presentationData.strings.ChatList_Search_FilterApps + icon = nil case .media: title = presentationData.strings.ChatList_Search_FilterMedia icon = nil diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 3c8642e60f5..31d76ca9f31 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -34,13 +34,14 @@ import AvatarNode private enum ChatListRecentEntryStableId: Hashable { case topPeers - case peerId(EnginePeer.Id) + case peerId(EnginePeer.Id, ChatListRecentEntry.Section) } private enum ChatListRecentEntry: Comparable, Identifiable { enum Section { case local case recommendedChannels + case popularApps } case topPeers([EnginePeer], PresentationTheme, PresentationStrings) @@ -50,8 +51,8 @@ private enum ChatListRecentEntry: Comparable, Identifiable { switch self { case .topPeers: return .topPeers - case let .peer(_, peer, _, _, _, _, _, _, _, _, _): - return .peerId(peer.peer.peerId) + case let .peer(_, peer, section, _, _, _, _, _, _, _, _): + return .peerId(peer.peer.peerId, section) } } @@ -108,7 +109,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable { animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, - isChannelsTabExpanded: Bool, + isChannelsTabExpanded: Bool?, toggleChannelsTabExpanded: @escaping () -> Void ) -> ListViewItem { switch self { @@ -183,7 +184,11 @@ private enum ChatListRecentEntry: Comparable, Identifiable { if user.flags.contains(.isSupport) && !servicePeer { status = .custom(string: strings.Bot_GenericSupportStatus, multiline: false, isActive: false, icon: nil) } else if let _ = user.botInfo { - status = .custom(string: strings.Bot_GenericBotStatus, multiline: false, isActive: false, icon: nil) + if let subscriberCount = user.subscriberCount { + status = .custom(string: strings.Conversation_StatusBotSubscribers(subscriberCount), multiline: false, isActive: false, icon: nil) + } else { + status = .custom(string: strings.Bot_GenericBotStatus, multiline: false, isActive: false, icon: nil) + } } else if user.id != context.account.peerId && !servicePeer { let presence = peer.presence ?? TelegramUserPresence(status: .none, lastActivity: 0) status = .presence(EnginePeer.Presence(presence), timeFormat) @@ -240,9 +245,25 @@ private enum ChatListRecentEntry: Comparable, Identifiable { if case .recommendedChannels = section { header = ChatListSearchItemHeader(type: .text(presentationData.strings.ChatList_Search_SectionRecommendedChannels, 1), theme: theme, strings: strings) } else { - header = ChatListSearchItemHeader(type: .text(presentationData.strings.ChatList_Search_SectionLocalChannels, 0), theme: theme, strings: strings, actionTitle: isChannelsTabExpanded ? presentationData.strings.ChatList_Search_SectionActionShowLess : presentationData.strings.ChatList_Search_SectionActionShowMore, action: { - toggleChannelsTabExpanded() - }) + if let isChannelsTabExpanded { + header = ChatListSearchItemHeader(type: .text(presentationData.strings.ChatList_Search_SectionLocalChannels, 0), theme: theme, strings: strings, actionTitle: isChannelsTabExpanded ? presentationData.strings.ChatList_Search_SectionActionShowLess : presentationData.strings.ChatList_Search_SectionActionShowMore, action: { + toggleChannelsTabExpanded() + }) + } else { + header = ChatListSearchItemHeader(type: .text(presentationData.strings.ChatList_Search_SectionLocalChannels, 0), theme: theme, strings: strings, actionTitle: nil, action: nil) + } + } + } else if case .apps = key { + if case .popularApps = section { + header = ChatListSearchItemHeader(type: .text(presentationData.strings.ChatList_Search_SectionPopularApps, 1), theme: theme, strings: strings) + } else { + if let isChannelsTabExpanded { + header = ChatListSearchItemHeader(type: .text(presentationData.strings.ChatList_Search_SectionRecentApps, 0), theme: theme, strings: strings, actionTitle: isChannelsTabExpanded ? presentationData.strings.ChatList_Search_SectionActionShowLess : presentationData.strings.ChatList_Search_SectionActionShowMore, action: { + toggleChannelsTabExpanded() + }) + } else { + header = ChatListSearchItemHeader(type: .text(presentationData.strings.ChatList_Search_SectionRecentApps, 0), theme: theme, strings: strings, actionTitle: nil, action: nil) + } } } else { header = ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear, action: { @@ -267,7 +288,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable { header: header, action: { _ in if let chatPeer = peer.peer.peers[peer.peer.peerId] { - peerSelected(EnginePeer(chatPeer), nil, section == .recommendedChannels) + peerSelected(EnginePeer(chatPeer), nil, section == .recommendedChannels || section == .popularApps) } }, disabledAction: { _ in @@ -276,10 +297,22 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } }, deletePeer: deletePeer, - contextAction: key == .channels ? nil : peerContextAction.flatMap { peerContextAction in + contextAction: (key == .channels || section == .popularApps) ? nil : peerContextAction.flatMap { peerContextAction in return { node, gesture, location in if let chatPeer = peer.peer.peers[peer.peer.peerId] { - peerContextAction(EnginePeer(chatPeer), .recentSearch, node, gesture, location) + let source: ChatListSearchContextActionSource + + if key == .apps { + if case .popularApps = section { + source = .popularApps + } else { + source = .recentApps + } + } else { + source = .recentSearch + } + + peerContextAction(EnginePeer(chatPeer), source, node, gesture, location) } else { gesture?.cancel() } @@ -708,6 +741,9 @@ public enum ChatListSearchEntry: Comparable, Identifiable { let headerType: ChatListSearchItemHeaderType if case .channels = key { headerType = .channels + } else if case .apps = key { + //TODO:localize + headerType = .text("APPS", AnyHashable("apps")) } else { if filter.contains(.onlyGroups) { headerType = .chats @@ -727,8 +763,17 @@ public enum ChatListSearchEntry: Comparable, Identifiable { if case .savedMessagesChats = location { isSavedMessages = true } + + var status: ContactsPeerItemStatus = .none + if case let .user(user) = primaryPeer, let _ = user.botInfo { + if let subscriberCount = user.subscriberCount { + status = .custom(string: presentationData.strings.Conversation_StatusBotSubscribers(subscriberCount), multiline: false, isActive: false, icon: nil) + } else { + status = .custom(string: presentationData.strings.Bot_GenericBotStatus, multiline: false, isActive: false, icon: nil) + } + } - return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: .none, badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { contactPeer in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: status, badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { contactPeer in if case let .peer(maybePeer, maybeChatPeer) = contactPeer, let peer = maybePeer, let chatPeer = maybeChatPeer { interaction.peerSelected(chatPeer, peer, nil, nil) } else { @@ -977,7 +1022,7 @@ private func chatListSearchContainerPreparedRecentTransition( animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, - isChannelsTabExpanded: Bool, + isChannelsTabExpanded: Bool?, toggleChannelsTabExpanded: @escaping () -> Void, isEmpty: Bool ) -> ChatListSearchContainerRecentTransition { @@ -1048,6 +1093,8 @@ private struct ChatListSearchMessagesContext { public enum ChatListSearchContextActionSource { case recentPeers case recentSearch + case recentApps + case popularApps case search(EngineMessage.Id?) } @@ -1225,6 +1272,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let tagMask: EngineMessage.Tags? private let location: ChatListControllerLocation private let navigationController: NavigationController? + private weak var parentController: ViewController? private let recentListNode: ListView private let shimmerNode: ChatListSearchShimmerNode @@ -1297,7 +1345,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var searchQueryDisposable: Disposable? private var searchOptionsDisposable: Disposable? - init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?, globalPeerSearchContext: GlobalPeerSearchContext?) { + init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?, parentController: ViewController?, globalPeerSearchContext: GlobalPeerSearchContext?) { self.context = context self.animationCache = animationCache self.animationRenderer = animationRenderer @@ -1305,6 +1353,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.key = key self.location = location self.navigationController = navigationController + self.parentController = parentController let globalPeerSearchContext = globalPeerSearchContext ?? GlobalPeerSearchContext() @@ -1327,6 +1376,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { tagMask = nil case .channels: tagMask = nil + case .apps: + tagMask = nil case .media: tagMask = .photoOrVideo case .downloads: @@ -1396,7 +1447,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.emptyResultsAnimationNode = DefaultAnimatedStickerNodeImpl() self.emptyResultsAnimationNode.isHidden = true - if key == .channels { + if key == .channels || key == .apps { let emptyRecentTitleNode = ImmediateTextNode() emptyRecentTitleNode.displaysAsynchronously = false emptyRecentTitleNode.attributedText = NSAttributedString(string: presentationData.strings.ChatList_Search_RecommendedChannelsEmpty_Title, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor) @@ -1409,7 +1460,11 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { emptyRecentTextNode.maximumNumberOfLines = 0 emptyRecentTextNode.textAlignment = .center emptyRecentTextNode.isHidden = true - emptyRecentTextNode.attributedText = NSAttributedString(string: presentationData.strings.ChatList_Search_RecommendedChannelsEmpty_Text, font: Font.regular(15.0), textColor: presentationData.theme.list.freeTextColor) + if key == .channels { + emptyRecentTextNode.attributedText = NSAttributedString(string: presentationData.strings.ChatList_Search_RecommendedChannelsEmpty_Text, font: Font.regular(15.0), textColor: presentationData.theme.list.freeTextColor) + } else if key == .apps { + emptyRecentTextNode.attributedText = NSAttributedString(string: presentationData.strings.ChatList_Search_Apps_Empty_Text, font: Font.regular(15.0), textColor: presentationData.theme.list.freeTextColor) + } self.emptyRecentTextNode = emptyRecentTextNode let emptyRecentAnimationNode = DefaultAnimatedStickerNodeImpl() @@ -1575,7 +1630,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let foundItems: Signal<([ChatListSearchEntry], Bool)?, NoError> = combineLatest(queue: .mainQueue(), searchQuery, searchOptions, downloadItems) |> mapToSignal { [weak self] query, options, downloadItems -> Signal<([ChatListSearchEntry], Bool)?, NoError> in - if query == nil && options == nil && [.chats, .topics, .channels].contains(key) { + if query == nil && options == nil && [.chats, .topics, .channels, .apps].contains(key) { let _ = currentRemotePeers.swap(nil) return .single(nil) } @@ -1868,6 +1923,109 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } + for id in matchingIds { + guard let maybePeer = peers[id], let peer = maybePeer else { + continue + } + resultPeers.append(EngineRenderedPeer(peer: peer)) + var isMuted = false + if let peerNotificationSettings = notificationSettings[peer.id] { + if case let .muted(until) = peerNotificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + isMuted = true + } else if case .default = peerNotificationSettings.muteState { + if case .user = peer { + isMuted = !globalNotificationSettings.privateChats.enabled + } else if case .legacyGroup = peer { + isMuted = !globalNotificationSettings.groupChats.enabled + } else if case let .channel(channel) = peer { + switch channel.info { + case .group: + isMuted = !globalNotificationSettings.groupChats.enabled + case .broadcast: + isMuted = !globalNotificationSettings.channels.enabled + } + } + } + } + let unreadCount = unreadCounts[peer.id] + if let unreadCount = unreadCount, unreadCount > 0 { + unread[peer.id] = (Int32(unreadCount), isMuted) + } + } + return (peers: resultPeers, unread: unread, recentlySearchedPeerIds: Set()) + } + } + } else if let query, key == .apps { + foundLocalPeers = combineLatest( + context.engine.peers.recentApps(), + context.engine.peers.recommendedAppPeerIds() + ) + |> mapToSignal { local, recommended -> Signal<(peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set), NoError> in + var peerIds: [EnginePeer.Id] = [] + + for peer in local { + if !peerIds.contains(peer) { + peerIds.append(peer) + } + } + if let recommended { + for id in recommended { + if !peerIds.contains(id) { + peerIds.append(id) + } + } + } + + return context.engine.data.subscribe( + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in + return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + } + ), + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.NotificationSettings in + return TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId) + } + ), + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Messages.PeerUnreadCount in + return TelegramEngine.EngineData.Item.Messages.PeerUnreadCount(id: peerId) + } + ), + TelegramEngine.EngineData.Item.NotificationSettings.Global() + ) + |> map { peers, notificationSettings, unreadCounts, globalNotificationSettings -> (peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set) in + var resultPeers: [EngineRenderedPeer] = [] + var unread: [EnginePeer.Id: (Int32, Bool)] = [:] + + let queryTokens = stringIndexTokens(query.lowercased(), transliteration: .combined) + + var matchingIds: [EnginePeer.Id] = [] + for peerId in local { + guard let maybePeer = peers[peerId], let peer = maybePeer else { + continue + } + if peer.indexName.matchesByTokens(queryTokens) { + if !matchingIds.contains(peerId) { + matchingIds.append(peerId) + } + } + } + + if let recommended { + for id in recommended { + guard let maybePeer = peers[id], let peer = maybePeer else { + continue + } + + if peer.indexName.matchesByTokens(queryTokens) { + if !matchingIds.contains(id) { + matchingIds.append(id) + } + } + } + } + for id in matchingIds { guard let maybePeer = peers[id], let peer = maybePeer else { continue @@ -1926,6 +2084,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { |> map { ($0.0, $0.1, false) } ) ) + } else if let query, case .apps = key { + let _ = query + foundRemotePeers = .single(([], [], false)) } else { foundRemotePeers = .single(([], [], false)) } @@ -1978,6 +2139,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) } else if peersFilter.contains(.doNotSearchMessages) { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) + } else if key == .apps { + foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) } else { if !finalQuery.isEmpty { addAppLogEvent(postbox: context.account.postbox, type: "search_global_query") @@ -2745,9 +2908,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openUrl: { url, _, _, message in interaction.openUrl(url) }, openInstantPage: { [weak self] message, data in - if let (webpage, anchor) = instantPageAndAnchor(message: message) { - let pageController = InstantPageController(context: context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .peer(message.id.peerId), peerType: .channel), anchor: anchor) - self?.navigationController?.pushViewController(pageController) + if let self, let navigationController = self.navigationController { + if let controller = self.context.sharedContext.makeInstantPageController(context: self.context, message: message, sourcePeerType: .channel) { + navigationController.pushViewController(controller) + } } }, longTap: { action, message in }, getHiddenMedia: { @@ -2990,6 +3154,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let hasRecentPeers: Signal if case .channels = key { hasRecentPeers = .single(false) + } else if case .apps = key { + hasRecentPeers = .single(false) } else { hasRecentPeers = context.engine.peers.recentPeers() |> map { value -> Bool in @@ -3005,7 +3171,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { struct RecentItems { var entries: [ChatListRecentEntry] - var isChannelsTabExpanded: Bool + var isChannelsTabExpanded: Bool? var recommendedChannelOrder: [EnginePeer.Id] var isEmpty: Bool } @@ -3094,19 +3260,19 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } - return .single(RecentItems(entries: entries, isChannelsTabExpanded: false, recommendedChannelOrder: [], isEmpty: false)) + return .single(RecentItems(entries: entries, isChannelsTabExpanded: nil, recommendedChannelOrder: [], isEmpty: false)) } if peersFilter.contains(.excludeRecent) { - recentItems = .single(RecentItems(entries: [], isChannelsTabExpanded: false, recommendedChannelOrder: [], isEmpty: false)) + recentItems = .single(RecentItems(entries: [], isChannelsTabExpanded: nil, recommendedChannelOrder: [], isEmpty: false)) } if case .savedMessagesChats = location { - recentItems = .single(RecentItems(entries: [], isChannelsTabExpanded: false, recommendedChannelOrder: [], isEmpty: false)) + recentItems = .single(RecentItems(entries: [], isChannelsTabExpanded: nil, recommendedChannelOrder: [], isEmpty: false)) } if case .channels = key { struct LocalChannels { var peerIds: [EnginePeer.Id] - var isExpanded: Bool + var isExpanded: Bool? } let localChannels = isChannelsTabExpandedValue.get() |> mapToSignal { isChannelsTabExpanded -> Signal in @@ -3278,6 +3444,164 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return RecentItems(entries: result, isChannelsTabExpanded: isChannelsTabExpanded, recommendedChannelOrder: recommendedChannelOrder, isEmpty: isEmpty) } } + } else if case .apps = key { + struct LocalApps { + var peerIds: [EnginePeer.Id] + var isExpanded: Bool? + } + let localApps = isChannelsTabExpandedValue.get() + |> mapToSignal { isChannelsTabExpanded -> Signal in + return context.engine.peers.recentApps() + |> map { peerIds -> LocalApps in + var isExpanded: Bool? = isChannelsTabExpanded + var peerIds = peerIds + if peerIds.count > 5 { + if !isChannelsTabExpanded { + peerIds = Array(peerIds.prefix(5)) + } + } else { + isExpanded = nil + } + return LocalApps(peerIds: peerIds, isExpanded: isExpanded) + } + } + + let remoteApps: Signal<[EnginePeer.Id]?, NoError> = context.engine.peers.recommendedAppPeerIds() + + let _ = self.context.engine.peers.requestRecommendedAppsIfNeeded().startStandalone() + + recentItems = combineLatest( + localApps, + remoteApps + ) + |> mapToSignal { localApps, remoteApps -> Signal in + var allAppIds = localApps.peerIds + + var recommendedAppOrder: [EnginePeer.Id] = [] + if let remoteApps { + for peerId in remoteApps { + if !allAppIds.contains(peerId) { + allAppIds.append(peerId) + } + recommendedAppOrder.append(peerId) + } + } + + return context.engine.data.subscribe( + EngineDataMap( + allAppIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in + return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + } + ), + EngineDataMap( + allAppIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.NotificationSettings in + return TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId) + } + ), + EngineDataMap( + allAppIds.map { peerId -> TelegramEngine.EngineData.Item.Messages.PeerUnreadCount in + return TelegramEngine.EngineData.Item.Messages.PeerUnreadCount(id: peerId) + } + ), + EngineDataMap( + allAppIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.StoryStats in + return TelegramEngine.EngineData.Item.Peer.StoryStats(id: peerId) + } + ), + EngineDataMap( + allAppIds.map { peerId -> TelegramEngine.EngineData.Item.Messages.PeerReadCounters in + return TelegramEngine.EngineData.Item.Messages.PeerReadCounters(id: peerId) + } + ), + TelegramEngine.EngineData.Item.NotificationSettings.Global() + ) + |> map { peers, notificationSettings, unreadCounts, storyStats, readCounters, globalNotificationSettings -> RecentItems in + var result: [ChatListRecentEntry] = [] + var existingIds = Set() + + for id in localApps.peerIds { + if existingIds.contains(id) { + continue + } + existingIds.insert(id) + guard let peer = peers[id], let peer else { + continue + } + let peerNotificationSettings = notificationSettings[id] + let subpeerSummary: RecentlySearchedPeerSubpeerSummary? = nil + var peerStoryStats: PeerStoryStats? + if let value = storyStats[peer.id] { + peerStoryStats = value + } + var unreadCount: Int32 = 0 + if let value = readCounters[peer.id] { + unreadCount = value.count + } + result.append(.peer( + index: result.count, + peer: RecentlySearchedPeer( + peer: RenderedPeer(peer: peer._asPeer()), + presence: nil, + notificationSettings: peerNotificationSettings.flatMap({ $0._asNotificationSettings() }), + unreadCount: unreadCount, + subpeerSummary: subpeerSummary + ), + .local, + presentationData.theme, + presentationData.strings, + presentationData.dateTimeFormat, + presentationData.nameSortOrder, + presentationData.nameDisplayOrder, + globalNotificationSettings, + peerStoryStats, + false + )) + } + if let remoteApps { + for appPeerId in remoteApps { + if existingIds.contains(appPeerId) { + continue + } + existingIds.insert(appPeerId) + guard let peer = peers[appPeerId], let peer else { + continue + } + let peerNotificationSettings = notificationSettings[appPeerId] + let subpeerSummary: RecentlySearchedPeerSubpeerSummary? = nil + var peerStoryStats: PeerStoryStats? + if let value = storyStats[peer.id] { + peerStoryStats = value + } + result.append(.peer( + index: result.count, + peer: RecentlySearchedPeer( + peer: RenderedPeer(peer: peer._asPeer()), + presence: nil, + notificationSettings: peerNotificationSettings.flatMap({ $0._asNotificationSettings() }), + unreadCount: 0, + subpeerSummary: subpeerSummary + ), + .popularApps, + presentationData.theme, + presentationData.strings, + presentationData.dateTimeFormat, + presentationData.nameSortOrder, + presentationData.nameDisplayOrder, + globalNotificationSettings, + peerStoryStats, + false + )) + } + } + + var isEmpty = false + if localApps.peerIds.isEmpty, let remoteApps, remoteApps.isEmpty { + isEmpty = true + } + + return RecentItems(entries: result, isChannelsTabExpanded: localApps.isExpanded, recommendedChannelOrder: recommendedAppOrder, isEmpty: isEmpty) + } + } } if case .chats = key, !peersFilter.contains(.excludeRecent) { @@ -3331,6 +3655,44 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { customChatNavigationStack: customChatNavigationStack )) } + } 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, + parentController: parentController, + updatedPresentationData: nil, + peer: peer, + threadId: nil, + buttonText: "", + url: "", + simple: true, + source: .generic, + skipTermsOfService: true + ) + } else { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + context: self.context, + chatLocation: .peer(peer), + keepStack: .always + )) + } + } } else { interaction.openPeer(peer, nil, threadId, true) if threadId == nil { @@ -3742,7 +4104,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { strongSelf.interaction.dismissInput() strongSelf.interaction.present(controller, nil) } else if case let .messages(chatLocation, _, _) = playlistLocation { - let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tag: .tag(EngineMessage.Tags.music)) + let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tag: .tag(EngineMessage.Tags.music)) var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } @@ -4000,7 +4362,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() - if isFirstTime && [.chats, .topics, .channels].contains(self.key) { + if isFirstTime && [.chats, .topics, .channels, .apps].contains(self.key) { options.insert(.PreferSynchronousDrawing) options.insert(.PreferSynchronousResourceLoading) } else if transition.animated { @@ -4073,7 +4435,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { strongSelf.emptyResultsAnimationNode.visibility = emptyResults } - var displayPlaceholder = transition.isLoading && (![.chats, .topics, .channels].contains(strongSelf.key) || (strongSelf.currentEntries?.isEmpty ?? true)) + var displayPlaceholder = transition.isLoading && (![.chats, .topics, .channels, .apps].contains(strongSelf.key) || (strongSelf.currentEntries?.isEmpty ?? true)) if strongSelf.key == .downloads { displayPlaceholder = false } @@ -4285,7 +4647,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) - let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", 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)) + let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", 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 timestamp1: Int32 = 100000 var peers: [EnginePeer.Id: EnginePeer] = [:] peers[peer1.id] = peer1 @@ -4309,7 +4671,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { let items = (0 ..< 2).compactMap { _ -> ListViewItem? in switch key { - case .chats, .topics, .channels, .downloads: + case .chats, .topics, .channels, .apps, .downloads: let message = EngineMessage( stableId: 0, stableVersion: 0, diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index 0b32ff7113a..28bc9b2f8be 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -51,6 +51,7 @@ public enum ChatListSearchPaneKey { case chats case topics case channels + case apps case media case downloads case links @@ -68,6 +69,8 @@ extension ChatListSearchPaneKey { return .topics case .channels: return .channels + case .apps: + return .apps case .media: return .media case .downloads: @@ -92,6 +95,7 @@ func defaultAvailableSearchPanes(isForum: Bool, hasDownloads: Bool) -> [ChatList result.append(.chats) } result.append(.channels) + result.append(.apps) result.append(contentsOf: [.media, .downloads, .links, .files, .music, .voice]) if !hasDownloads { @@ -122,6 +126,7 @@ private final class ChatListSearchPendingPane { updatedPresentationData: (initial: PresentationData, signal: Signal)?, interaction: ChatListSearchInteraction, navigationController: NavigationController?, + parentController: ViewController?, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, @@ -131,7 +136,7 @@ private final class ChatListSearchPendingPane { key: ChatListSearchPaneKey, hasBecomeReady: @escaping (ChatListSearchPaneKey) -> Void ) { - let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: (key == .chats || key == .topics) ? peersFilter : [], requestPeerType: requestPeerType, location: location, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController, globalPeerSearchContext: globalPeerSearchContext) + let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: (key == .chats || key == .topics) ? peersFilter : [], requestPeerType: requestPeerType, location: location, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController, parentController: parentController, globalPeerSearchContext: globalPeerSearchContext) self.pane = ChatListSearchPaneWrapper(key: key, node: paneNode) self.disposable = (paneNode.isReady @@ -154,11 +159,12 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD private let updatedPresentationData: (initial: PresentationData, signal: Signal)? private let peersFilter: ChatListNodePeersFilter private let requestPeerType: [ReplyMarkupButtonRequestPeerType]? - private let location: ChatListControllerLocation + var location: ChatListControllerLocation private let searchQuery: Signal private let searchOptions: Signal private let globalPeerSearchContext: GlobalPeerSearchContext private let navigationController: NavigationController? + private weak var parentController: ViewController? var interaction: ChatListSearchInteraction? let isReady = Promise() @@ -186,10 +192,11 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD var currentPaneUpdated: ((ChatListSearchPaneKey?, CGFloat, ContainedViewLayoutTransition) -> Void)? var requestExpandTabs: (() -> Bool)? + var requesDismissInput: (() -> Void)? private var currentAvailablePanes: [ChatListSearchPaneKey]? - init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { + init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?, parentController: ViewController?) { self.context = context self.animationCache = animationCache self.animationRenderer = animationRenderer @@ -200,6 +207,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD self.searchQuery = searchQuery self.searchOptions = searchOptions self.navigationController = navigationController + self.parentController = parentController self.globalPeerSearchContext = GlobalPeerSearchContext() super.init() @@ -220,12 +228,20 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD if let (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes) = self.currentParams { self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, availablePanes: availablePanes, transition: .animated(duration: 0.4, curve: .spring)) } + + if case .apps = key { + self.requesDismissInput?() + } } else if self.pendingSwitchToPaneKey != key { self.pendingSwitchToPaneKey = key if let (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes) = self.currentParams { self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, availablePanes: availablePanes, transition: .animated(duration: 0.4, curve: .spring)) } + + if case .apps = key { + self.requesDismissInput?() + } } } @@ -315,6 +331,10 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD let switchToKey = availablePanes[updatedIndex] if switchToKey != self.currentPaneKey && self.currentPanes[switchToKey] != nil{ self.currentPaneKey = switchToKey + + if case .apps = switchToKey { + self.requesDismissInput?() + } } } self.transitionFraction = 0.0 @@ -430,6 +450,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD updatedPresentationData: self.updatedPresentationData, interaction: self.interaction!, navigationController: self.navigationController, + parentController: self.parentController, peersFilter: self.peersFilter, requestPeerType: self.requestPeerType, location: self.location, diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index 2009fc63d01..1bd0168a826 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -150,7 +150,7 @@ public final class ChatListShimmerNode: ASDisplayNode { let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) - let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", 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)) + let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", 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 timestamp1: Int32 = 100000 let peers: [EnginePeer.Id: EnginePeer] = [:] let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index dcf9d615326..29fe4114748 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2350,14 +2350,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if let forwardInfo = message.forwardInfo { effectiveAuthor = forwardInfo.author if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature { - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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) } } if let sourceAuthorInfo = message._asMessage().sourceAuthorInfo { if let originalAuthor = sourceAuthorInfo.originalAuthor, let peer = message.peers[originalAuthor] { effectiveAuthor = peer } else if let authorSignature = sourceAuthorInfo.originalAuthorName { - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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) } } @@ -2647,7 +2647,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { case let .preview(dimensions, immediateThumbnailData, videoDuration): if let immediateThumbnailData { if let videoDuration { - let thumbnailMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: index), partialReference: nil, resource: EmptyMediaResource(), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Video(duration: Double(videoDuration), size: dimensions ?? PixelDimensions(width: 1, height: 1), flags: [], preloadSize: nil)]) + let thumbnailMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: index), partialReference: nil, resource: EmptyMediaResource(), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Video(duration: Double(videoDuration), size: dimensions ?? PixelDimensions(width: 1, height: 1), flags: [], preloadSize: nil, coverTime: nil)]) contentImageSpecs.append(ContentImageSpec(message: message, media: .file(thumbnailMedia), size: fitSize)) } else { let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: index), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) @@ -4812,7 +4812,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - guard let item = self.item else { + guard let item = self.item, self.frame.height > 0.0 else { return nil } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index 5d0f029f297..4dc4de1c29a 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -246,7 +246,7 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: processed = true break inner } - case let .Video(_, _, flags, _): + case let .Video(_, _, flags, _, _): if flags.contains(.instantRoundVideo) { messageText = strings.Message_VideoMessage processed = true diff --git a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift index d1075fb80f0..b3f88f9b8cb 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift @@ -312,6 +312,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { avatarsNode = current } else { avatarsNode = MergedAvatarsNode() + avatarsNode.isUserInteractionEnabled = false strongSelf.addSubnode(avatarsNode) strongSelf.avatarsNode = avatarsNode } diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 0b947f19102..4a0bc1b679d 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -61,13 +61,6 @@ public enum ChatTranslationDisplayType { case translated } -public enum ChatOpenWebViewSource: Equatable { - case generic - case menu - case inline(bot: EnginePeer) - case webApp(botApp: BotApp) -} - public final class ChatPanelInterfaceInteraction { // MARK: Nicegram public let cloudMessages: ([Message]?) -> Void diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift index 402ddcf61aa..5c5647b2f12 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift @@ -20,6 +20,8 @@ public enum SendMessageActionSheetControllerParams { public let attachment: Bool public let canSendWhenOnline: Bool public let forwardMessageIds: [EngineMessage.Id] + public let canMakePaidContent: Bool + public let currentPrice: Int64? public init( isScheduledMessages: Bool, @@ -28,7 +30,9 @@ public enum SendMessageActionSheetControllerParams { messageEffect: (ChatSendMessageActionSheetControllerSendParameters.Effect?, (ChatSendMessageActionSheetControllerSendParameters.Effect?) -> Void)?, attachment: Bool, canSendWhenOnline: Bool, - forwardMessageIds: [EngineMessage.Id] + forwardMessageIds: [EngineMessage.Id], + canMakePaidContent: Bool, + currentPrice: Int64? ) { self.isScheduledMessages = isScheduledMessages self.mediaPreview = mediaPreview @@ -37,6 +41,8 @@ public enum SendMessageActionSheetControllerParams { self.attachment = attachment self.canSendWhenOnline = canSendWhenOnline self.forwardMessageIds = forwardMessageIds + self.canMakePaidContent = canMakePaidContent + self.currentPrice = currentPrice } } @@ -74,6 +80,7 @@ public func makeChatSendMessageActionSheetController( completion: @escaping () -> Void, sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void, + editPrice: @escaping (Int64) -> Void, openPremiumPaywall: @escaping (ViewController) -> Void, reactionItems: [ReactionItem]? = nil, availableMessageEffects: AvailableMessageEffects? = nil, @@ -97,6 +104,7 @@ public func makeChatSendMessageActionSheetController( completion: completion, sendMessage: sendMessage, schedule: schedule, + editPrice: editPrice, openPremiumPaywall: openPremiumPaywall, reactionItems: reactionItems, availableMessageEffects: availableMessageEffects, diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift index bef975e7d95..e5d074f9449 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift @@ -98,6 +98,7 @@ final class ChatSendMessageContextScreenComponent: Component { let completion: () -> Void let sendMessage: (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void let schedule: (ChatSendMessageActionSheetController.SendParameters?) -> Void + let editPrice: (Int64) -> Void let openPremiumPaywall: (ViewController) -> Void let reactionItems: [ReactionItem]? let availableMessageEffects: AvailableMessageEffects? @@ -121,6 +122,7 @@ final class ChatSendMessageContextScreenComponent: Component { completion: @escaping () -> Void, sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void, + editPrice: @escaping (Int64) -> Void, openPremiumPaywall: @escaping (ViewController) -> Void, reactionItems: [ReactionItem]?, availableMessageEffects: AvailableMessageEffects?, @@ -144,6 +146,7 @@ final class ChatSendMessageContextScreenComponent: Component { self.completion = completion self.sendMessage = sendMessage self.schedule = schedule + self.editPrice = editPrice self.openPremiumPaywall = openPremiumPaywall self.reactionItems = reactionItems self.availableMessageEffects = availableMessageEffects @@ -475,6 +478,8 @@ final class ChatSendMessageContextScreenComponent: Component { var reminders = false var isSecret = false var canSchedule = false + var canMakePaidContent = false + var currentPrice: Int64? switch component.params { case let .sendMessage(sendMessage): if let peerId = component.peerId { @@ -485,6 +490,8 @@ final class ChatSendMessageContextScreenComponent: Component { if sendMessage.isScheduledMessages { canSchedule = false } + canMakePaidContent = sendMessage.canMakePaidContent + currentPrice = sendMessage.currentPrice case .editMessage: break } @@ -604,6 +611,38 @@ final class ChatSendMessageContextScreenComponent: Component { } ))) } + if canMakePaidContent { + let title: String + let titleLayout: ContextMenuActionItemTextLayout + if let currentPrice { + title = environment.strings.Attachment_Paid_EditPrice + titleLayout = .secondLineWithValue(environment.strings.Attachment_Paid_EditPrice_Stars(Int32(currentPrice))) + } else { + title = environment.strings.Attachment_Paid_Create + titleLayout = .twoLinesMax + } + items.append(.action(ContextMenuActionItem( + id: AnyHashable("paid"), + text: title, + textLayout: titleLayout, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, _ in + guard let self, let component = self.component, case let .sendMessage(params) = component.params else { + return + } + + let editPrice = component.editPrice + let controller = component.context.sharedContext.makeStarsAmountScreen(context: component.context, initialValue: params.currentPrice, completion: { amount in + editPrice(amount) + }) + self.environment?.controller()?.dismiss() + Queue.mainQueue().after(0.45) { + component.openPremiumPaywall(controller) + } + } + ))) + } case .editMessage: items.append(.action(ContextMenuActionItem( id: AnyHashable("silent"), @@ -1471,6 +1510,7 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha completion: @escaping () -> Void, sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void, + editPrice: @escaping (Int64) -> Void, openPremiumPaywall: @escaping (ViewController) -> Void, reactionItems: [ReactionItem]?, availableMessageEffects: AvailableMessageEffects?, @@ -1498,6 +1538,7 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha completion: completion, sendMessage: sendMessage, schedule: schedule, + editPrice: editPrice, openPremiumPaywall: openPremiumPaywall, reactionItems: reactionItems, availableMessageEffects: availableMessageEffects, diff --git a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift index 8ed99a1fb87..c802ca3928e 100644 --- a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift +++ b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift @@ -433,7 +433,7 @@ public func chatTextLinkEditController(sharedContext: SharedAccountContext, upda return } let updatedLink = explicitUrl(contentNode.link) - if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false]) { + if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false, "tonsite": true]) { dismissImpl?(true) apply(updatedLink) } else if allowEmpty && contentNode.link.isEmpty { diff --git a/submodules/ComponentFlow/Source/Components/Button.swift b/submodules/ComponentFlow/Source/Components/Button.swift index c599f0ee6ca..0ac1ac1dddf 100644 --- a/submodules/ComponentFlow/Source/Components/Button.swift +++ b/submodules/ComponentFlow/Source/Components/Button.swift @@ -9,7 +9,7 @@ public final class Button: Component { public let isEnabled: Bool public let isExclusive: Bool public let action: () -> Void - public let holdAction: (() -> Void)? + public let holdAction: ((UIView) -> Void)? public let highlightedAction: ActionSlot? convenience public init( @@ -39,7 +39,7 @@ public final class Button: Component { isEnabled: Bool = true, isExclusive: Bool = true, action: @escaping () -> Void, - holdAction: (() -> Void)?, + holdAction: ((UIView) -> Void)?, highlightedAction: ActionSlot? ) { self.content = content @@ -82,7 +82,7 @@ public final class Button: Component { } - public func withHoldAction(_ holdAction: (() -> Void)?) -> Button { + public func withHoldAction(_ holdAction: ((UIView) -> Void)?) -> Button { return Button( content: self.content, minSize: self.minSize, @@ -228,7 +228,7 @@ public final class Button: Component { return } strongSelf.holdActionTimer?.invalidate() - strongSelf.component?.holdAction?() + strongSelf.component?.holdAction?(strongSelf) strongSelf.beginExecuteHoldActionTimer() }) self.holdActionTimer = holdActionTimer @@ -246,7 +246,7 @@ public final class Button: Component { guard let strongSelf = self else { return } - strongSelf.component?.holdAction?() + strongSelf.component?.holdAction?(strongSelf) }) self.holdActionTimer = holdActionTimer RunLoop.main.add(holdActionTimer, forMode: .common) diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 7d4154d9101..1e37b9b72a4 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -21,10 +21,12 @@ private let tagImage: UIImage? = { }() 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) { @@ -35,7 +37,45 @@ private final class StarsButtonEffectLayer: 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) } } diff --git a/submodules/ComposePollUI/Sources/ComposePollScreen.swift b/submodules/ComposePollUI/Sources/ComposePollScreen.swift index 526009e4c6e..19026f35d17 100644 --- a/submodules/ComposePollUI/Sources/ComposePollScreen.swift +++ b/submodules/ComposePollUI/Sources/ComposePollScreen.swift @@ -1483,6 +1483,8 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont fileprivate private(set) var sendButtonItem: UIBarButtonItem? + public var isMinimized: Bool = false + public var requestAttachmentMenuExpansion: () -> Void = { } public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in diff --git a/submodules/ComposePollUI/Sources/CreatePollController.swift b/submodules/ComposePollUI/Sources/CreatePollController.swift index ff3f8cc6349..91d027f8991 100644 --- a/submodules/ComposePollUI/Sources/CreatePollController.swift +++ b/submodules/ComposePollUI/Sources/CreatePollController.swift @@ -532,44 +532,7 @@ public final class ComposedPoll { } private final class CreatePollContext: AttachmentMediaPickerContext { - var selectionCount: Signal { - return .single(0) - } - - var caption: Signal { - return .single(nil) - } - - var captionIsAboveMedia: Signal { - return .single(false) - } - - var hasCaption: Bool { - return false - } - - func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { - } - - public var loadingProgress: Signal { - return .single(nil) - } - - public var mainButtonState: Signal { - return .single(nil) - } - - func setCaption(_ caption: NSAttributedString) { - } - - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { - } - - func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { - } - - func mainButtonAction() { - } + } @@ -584,6 +547,7 @@ public class CreatePollControllerImpl: ItemListController, AttachmentContainable public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } + public var isMinimized: Bool = false public var mediaPickerContext: AttachmentMediaPickerContext? { return CreatePollContext() diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index 82dde2f1fdc..dfc028f9ff4 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -569,7 +569,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis if !topPeers.isEmpty { var index: Int = 0 var sectionId: Int = 1 - for (title, peerIds) in sections { + for (title, peerIds, hasActions) in sections { var allSelected = true if let selectedPeerIndices = selectionState?.selectedPeerIndices, !selectedPeerIndices.isEmpty { for peerId in peerIds { @@ -617,7 +617,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis } let presence = presences[peer.id] - entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, true, nil, false)) + entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, hasActions, true, nil, false)) index += 1 } @@ -629,7 +629,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis if !sections.isEmpty, let selectionState { var hasNonBirthdayPeers = false var allBirthdayPeerIds = Set() - for (_, peerIds) in sections { + for (_, peerIds, _) in sections { for peerId in peerIds { allBirthdayPeerIds.insert(peerId) } @@ -865,7 +865,7 @@ public enum ContactListPresentation { public enum TopPeers { case none case recent - case custom([(title: String, peerIds: [EnginePeer.Id])]) + case custom([(title: String, peerIds: [EnginePeer.Id], hasActions: Bool)]) } case orderedByPresence(options: [ContactListAdditionalOption]) @@ -1500,6 +1500,8 @@ public final class ContactListNode: ASDisplayNode { disabledPeerIds = disabledPeerIds.union(peerIds) case .excludeWithoutPhoneNumbers: requirePhoneNumbers = true + case .excludeBots: + break } } @@ -1712,7 +1714,7 @@ public final class ContactListNode: ASDisplayNode { } case let .custom(sections): var peerIds: [EnginePeer.Id] = [] - for (_, sectionPeers) in sections { + for (_, sectionPeers, _) in sections { peerIds.append(contentsOf: sectionPeers) } topPeers = combineLatest( @@ -1787,6 +1789,8 @@ public final class ContactListNode: ASDisplayNode { disabledPeerIds = disabledPeerIds.union(peerIds) case .excludeWithoutPhoneNumbers: requirePhoneNumbers = true + case .excludeBots: + break } } diff --git a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift index acda9b45281..9056670c024 100644 --- a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift @@ -394,6 +394,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo var existingPeerIds = Set() var disabledPeerIds = Set() var requirePhoneNumbers = false + var excludeBots = false for filter in filters { switch filter { case .excludeSelf: @@ -404,6 +405,8 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo disabledPeerIds = disabledPeerIds.union(peerIds) case .excludeWithoutPhoneNumbers: requirePhoneNumbers = true + case .excludeBots: + excludeBots = true } } var existingNormalizedPhoneNumbers = Set() @@ -413,10 +416,17 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo continue } - if case let .user(user) = peer, requirePhoneNumbers { - let phone = user.phone ?? "" - if phone.isEmpty { - continue + if case let .user(user) = peer { + if requirePhoneNumbers { + let phone = user.phone ?? "" + if phone.isEmpty { + continue + } + } + if excludeBots { + if user.botInfo != nil { + continue + } } } @@ -442,11 +452,18 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo continue } - if let user = peer.peer as? TelegramUser, requirePhoneNumbers { - let phone = user.phone ?? "" - if phone.isEmpty { - continue + if let user = peer.peer as? TelegramUser { + if requirePhoneNumbers { + let phone = user.phone ?? "" + if phone.isEmpty { + continue + } } + if excludeBots { + if user.botInfo != nil { + continue + } + } } if !existingPeerIds.contains(peer.peer.id) { diff --git a/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift b/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift index d3127070f0e..29d1cba6085 100644 --- a/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift @@ -56,7 +56,7 @@ private enum InviteContactsEntry: Comparable, Identifiable { } else { status = .none } - let peer: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: contact.firstName, lastName: contact.lastName, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) + let peer: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: contact.firstName, lastName: contact.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)) return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), status: status, enabled: true, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in interaction.toggleContact(id) }) diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 663fca5d618..b6e72f5525a 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -868,7 +868,11 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { break case let .presence(presence, dateTimeFormat): if case let .peer(peer, _) = item.peer, let peer, case let .user(user) = peer, user.botInfo != nil { - statusAttributedString = NSAttributedString(string: item.presentationData.strings.Bot_GenericBotStatus, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + if let subscriberCount = user.subscriberCount { + statusAttributedString = NSAttributedString(string: item.presentationData.strings.Conversation_StatusBotSubscribers(subscriberCount), font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + } else { + statusAttributedString = NSAttributedString(string: item.presentationData.strings.Bot_GenericBotStatus, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + } } else { userPresence = presence let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 8bc7ec79d10..ab4ac828c77 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -104,7 +104,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case knockoutWallpaper(PresentationTheme, Bool) case experimentalCompatibility(Bool) case enableDebugDataDisplay(Bool) - case acceleratedStickers(Bool) + case rippleEffect(Bool) case browserExperiment(Bool) case localTranscription(Bool) case enableReactionOverrides(Bool) @@ -143,7 +143,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.web.rawValue case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue - case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2: + case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue @@ -238,7 +238,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 37 case .enableDebugDataDisplay: return 38 - case .acceleratedStickers: + case .rippleEffect: return 39 case .browserExperiment: return 40 @@ -1283,12 +1283,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) - case let .acceleratedStickers(value): - return ItemListSwitchItem(presentationData: presentationData, title: "Accelerated Stickers", value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .rippleEffect(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Ripple", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings - settings.acceleratedStickers = value + settings.rippleEffect = value return PreferencesEntry(settings) }) }).start() @@ -1529,7 +1529,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.knockoutWallpaper(presentationData.theme, experimentalSettings.knockoutWallpaper)) entries.append(.experimentalCompatibility(experimentalSettings.experimentalCompatibility)) entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay)) - entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers)) + entries.append(.rippleEffect(experimentalSettings.rippleEffect)) #if DEBUG entries.append(.browserExperiment(experimentalSettings.browserExperiment)) #else diff --git a/submodules/DeviceAccess/Sources/DeviceAccess.swift b/submodules/DeviceAccess/Sources/DeviceAccess.swift index 6fba228c803..cfa335f52dc 100644 --- a/submodules/DeviceAccess/Sources/DeviceAccess.swift +++ b/submodules/DeviceAccess/Sources/DeviceAccess.swift @@ -36,6 +36,7 @@ public enum DeviceAccessLocationSubject { case send case live case tracking + case weather } public enum DeviceAccessSubject { @@ -352,24 +353,26 @@ public final class DeviceAccess { } else { completion(true) } - } else if [.restricted, .denied].contains(status), let presentationData = presentationData { - let text: String - if case .restricted = status { - text = presentationData.strings.AccessDenied_CameraRestricted - } else { - switch cameraSubject { - case .video: - text = presentationData.strings.AccessDenied_Camera - case .videoCall: - text = presentationData.strings.AccessDenied_VideoCallCamera - case .qrCode: - text = presentationData.strings.AccessDenied_QrCamera + } else if [.restricted, .denied].contains(status) { + completion(false) + if let presentationData = presentationData { + let text: String + if case .restricted = status { + text = presentationData.strings.AccessDenied_CameraRestricted + } else { + switch cameraSubject { + case .video: + text = presentationData.strings.AccessDenied_Camera + case .videoCall: + text = presentationData.strings.AccessDenied_VideoCallCamera + case .qrCode: + text = presentationData.strings.AccessDenied_QrCamera + } } + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + openSettings() + })]), nil) } - completion(false) - present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { - openSettings() - })]), nil) } else if case .authorized = status { completion(true) } else { @@ -474,7 +477,7 @@ public final class DeviceAccess { } case .authorizedWhenInUse: switch locationSubject { - case .send, .tracking: + case .send, .tracking, .weather: completion(true) case .live: completion(false) @@ -495,6 +498,8 @@ public final class DeviceAccess { text = presentationData.strings.AccessDenied_LocationDenied case .tracking: text = presentationData.strings.AccessDenied_LocationTracking + case .weather: + text = presentationData.strings.AccessDenied_LocationWeather } } else { text = presentationData.strings.AccessDenied_LocationDisabled @@ -505,7 +510,7 @@ public final class DeviceAccess { } case .notDetermined: switch locationSubject { - case .send, .tracking: + case .send, .tracking, .weather: locationManager?.requestWhenInUseAuthorization(completion: { status in completion(status == .authorizedWhenInUse || status == .authorizedAlways) }) diff --git a/submodules/Display/Source/ContainerViewLayout.swift b/submodules/Display/Source/ContainerViewLayout.swift index 8325f282588..ba1894abd4b 100644 --- a/submodules/Display/Source/ContainerViewLayout.swift +++ b/submodules/Display/Source/ContainerViewLayout.swift @@ -90,6 +90,10 @@ public struct ContainerViewLayout: Equatable { return ContainerViewLayout(size: self.size, metrics: self.metrics, deviceMetrics: self.deviceMetrics, intrinsicInsets: intrinsicInsets, safeInsets: self.safeInsets, additionalInsets: self.additionalInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver) } + public func withUpdatedAdditionalInsets(_ additionalInsets: UIEdgeInsets) -> ContainerViewLayout { + return ContainerViewLayout(size: self.size, metrics: self.metrics, deviceMetrics: self.deviceMetrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, additionalInsets: additionalInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver) + } + public func withUpdatedInputHeight(_ inputHeight: CGFloat?) -> ContainerViewLayout { return ContainerViewLayout(size: self.size, metrics: self.metrics, deviceMetrics: self.deviceMetrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, additionalInsets: self.additionalInsets, statusBarHeight: self.statusBarHeight, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver) } diff --git a/submodules/Display/Source/Navigation/MinimizedContainer.swift b/submodules/Display/Source/Navigation/MinimizedContainer.swift index b8b5b2b1fac..129a9e7b1e4 100644 --- a/submodules/Display/Source/Navigation/MinimizedContainer.swift +++ b/submodules/Display/Source/Navigation/MinimizedContainer.swift @@ -3,16 +3,85 @@ import AsyncDisplayKit public protocol MinimizedContainer: ASDisplayNode { var navigationController: NavigationController? { get set } - var controllers: [ViewController] { get } + var controllers: [MinimizableController] { get } var isExpanded: Bool { get } - var willMaximize: (() -> Void)? { get set } + var willMaximize: ((MinimizedContainer) -> Void)? { get set } + var willDismiss: ((MinimizedContainer) -> Void)? { get set } + var didDismiss: ((MinimizedContainer) -> Void)? { get set } - func addController(_ viewController: ViewController, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void, transition: ContainedViewLayoutTransition) - func maximizeController(_ viewController: ViewController, animated: Bool, completion: @escaping (Bool) -> Void) + var statusBarStyle: StatusBarStyle { get } + var statusBarStyleUpdated: (() -> Void)? { get set } + + func addController(_ viewController: MinimizableController, topEdgeOffset: CGFloat?, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void, transition: ContainedViewLayoutTransition) + func removeController(_ viewController: MinimizableController) + func maximizeController(_ viewController: MinimizableController, animated: Bool, completion: @escaping (Bool) -> Void) func collapse() func dismissAll(completion: @escaping () -> Void) func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) func collapsedHeight(layout: ContainerViewLayout) -> CGFloat } + +public protocol MinimizableController: ViewController { + var minimizedTopEdgeOffset: CGFloat? { get } + var minimizedBounds: CGRect? { get } + var isMinimized: Bool { get set } + var isMinimizable: Bool { get } + var minimizedIcon: UIImage? { get } + var minimizedProgress: Float? { get } + + func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) + func makeContentSnapshotView() -> UIView? + + func prepareContentSnapshotView() + func resetContentSnapshotView() + + func shouldDismissImmediately() -> Bool +} + +public extension MinimizableController { + var minimizedTopEdgeOffset: CGFloat? { + return nil + } + + var minimizedBounds: CGRect? { + return nil + } + + var isMinimized: Bool { + return false + } + + var isMinimizable: Bool { + return false + } + + var minimizedIcon: UIImage? { + return nil + } + + var minimizedProgress: Float? { + return nil + } + + func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) { + + } + + func makeContentSnapshotView() -> UIView? { + return self.displayNode.view.snapshotView(afterScreenUpdates: false) + } + + func prepareContentSnapshotView() { + + } + + func resetContentSnapshotView() { + + } + + func shouldDismissImmediately() -> Bool { + return true + } +} diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index 45d9f2f3002..60c371dd67e 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -153,13 +153,29 @@ open class NavigationController: UINavigationController, ContainableController, open var minimizedContainer: MinimizedContainer? { didSet { self.minimizedContainer?.navigationController = self - self.minimizedContainer?.willMaximize = { [weak self] in + self.minimizedContainer?.willMaximize = { [weak self] _ in guard let self else { return } self.isMaximizing = true self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring)) } + self.minimizedContainer?.willDismiss = { [weak self] _ in + guard let self else { + return + } + self.minimizedContainer = nil + self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring)) + } + self.minimizedContainer?.didDismiss = { minimizedContainer in + minimizedContainer.removeFromSupernode() + } + self.minimizedContainer?.statusBarStyleUpdated = { [weak self] in + guard let self else { + return + } + self.updateContainersNonReentrant(transition: .animated(duration: 0.3, curve: .easeInOut)) + } } } @@ -437,7 +453,20 @@ open class NavigationController: UINavigationController, ContainableController, globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1.0)) } - let overlayContainerLayout = layout + var overlayContainerLayout = layout + + var updatedSize = layout.size + var updatedIntrinsicInsets = layout.intrinsicInsets + var updatedAdditionalInsets = layout.additionalInsets + if let minimizedContainer = self.minimizedContainer { + if (layout.inputHeight ?? 0.0).isZero { + let minimizedContainerHeight = minimizedContainer.collapsedHeight(layout: layout) + updatedSize.height -= minimizedContainerHeight + updatedIntrinsicInsets.bottom = 0.0 + updatedAdditionalInsets.bottom += minimizedContainerHeight + } + } + overlayContainerLayout = overlayContainerLayout.withUpdatedAdditionalInsets(updatedAdditionalInsets) if let inCallStatusBar = self.inCallStatusBar { let isLandscape = layout.size.width > layout.size.height @@ -837,8 +866,6 @@ open class NavigationController: UINavigationController, ContainableController, layout.additionalInsets.left = max(layout.intrinsicInsets.left, additionalSideInsets.left) layout.additionalInsets.right = max(layout.intrinsicInsets.right, additionalSideInsets.right) - var updatedSize = layout.size - var updatedIntrinsicInsets = layout.intrinsicInsets if case .flat = navigationLayout.root, let minimizedContainer = self.minimizedContainer { if minimizedContainer.supernode !== self.displayNode { if let rootContainer = self.rootContainer, case let .flat(flatContainer) = rootContainer { @@ -851,10 +878,6 @@ open class NavigationController: UINavigationController, ContainableController, self.displayNode.insertSubnode(minimizedContainer, at: 0) } } - if (layout.inputHeight ?? 0.0).isZero { - updatedSize.height -= minimizedContainer.collapsedHeight(layout: layout) - updatedIntrinsicInsets.bottom = 0.0 - } } switch navigationLayout.root { @@ -1165,6 +1188,16 @@ open class NavigationController: UINavigationController, ContainableController, statusBarHidden = true } + if let minimizedContainer = self.minimizedContainer, minimizedContainer.isExpanded { + if case .Hide = minimizedContainer.statusBarStyle { + statusBarHidden = true + statusBarStyle = .White + } else { + statusBarHidden = false + statusBarStyle = minimizedContainer.statusBarStyle + } + } + let resolvedStatusBarStyle: NavigationStatusBarStyle switch statusBarStyle { case .Ignore, .Hide: @@ -1577,19 +1610,11 @@ open class NavigationController: UINavigationController, ContainableController, self._viewControllersPromise.set(self.viewControllers) } - public func minimizeViewController(_ viewController: ViewController, damping: CGFloat?, velocity: CGFloat? = nil, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void, setupContainer: (MinimizedContainer?) -> MinimizedContainer?, animated: Bool) { + public func minimizeViewController(_ viewController: MinimizableController, topEdgeOffset: CGFloat? = nil, damping: CGFloat? = nil, velocity: CGFloat? = nil, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void, setupContainer: (MinimizedContainer?) -> MinimizedContainer?, animated: Bool) { let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .customSpring(damping: damping ?? 124.0, initialVelocity: velocity ?? 0.0)) : .immediate let minimizedContainer = setupContainer(self.minimizedContainer) if self.minimizedContainer !== minimizedContainer { - minimizedContainer?.willMaximize = { [weak self] in - guard let self else { - return - } - self.isMaximizing = true - self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring)) - } - self.minimizedContainer?.removeFromSupernode() self.minimizedContainer = minimizedContainer @@ -1597,11 +1622,11 @@ open class NavigationController: UINavigationController, ContainableController, } viewController.isMinimized = true self.filterController(viewController, animated: true) - minimizedContainer?.addController(viewController, beforeMaximize: beforeMaximize, transition: transition) + minimizedContainer?.addController(viewController, topEdgeOffset: topEdgeOffset, beforeMaximize: beforeMaximize, transition: transition) } private var isMaximizing = false - public func maximizeViewController(_ viewController: ViewController, animated: Bool) { + public func maximizeViewController(_ viewController: MinimizableController, animated: Bool) { guard let minimizedContainer = self.minimizedContainer else { return } diff --git a/submodules/Display/Source/Navigation/NavigationLayout.swift b/submodules/Display/Source/Navigation/NavigationLayout.swift index 839c4f41ea4..61cfc02b45b 100644 --- a/submodules/Display/Source/Navigation/NavigationLayout.swift +++ b/submodules/Display/Source/Navigation/NavigationLayout.swift @@ -50,6 +50,15 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL case .regular: requiresModal = true } + case .modalInCompactLayout: + switch layout.metrics.widthClass { + case .compact: + requiresModal = true + case .regular: + requiresModal = true + beginsModal = true + isFlat = true + } } if requiresModal { controller._presentedInModal = true diff --git a/submodules/Display/Source/Navigation/NavigationModalContainer.swift b/submodules/Display/Source/Navigation/NavigationModalContainer.swift index 65128341fa7..185ac4432bf 100644 --- a/submodules/Display/Source/Navigation/NavigationModalContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationModalContainer.swift @@ -90,6 +90,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.clipsToBounds = false self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.tag = 0x5C4011 let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in guard let strongSelf = self, !strongSelf.isDismissed else { @@ -245,6 +246,8 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes self.view.endEditing(true) } + private var isDraggingHeader = false + func scrollViewDidScroll(_ scrollView: UIScrollView) { if self.ignoreScrolling || self.isDismissed { return @@ -253,6 +256,9 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes progress = max(0.0, min(1.0, progress)) self.dismissProgress = progress self.applyDismissProgress(transition: .immediate, completion: {}) + + let location = scrollView.panGestureRecognizer.location(in: scrollView).offsetBy(dx: 0.0, dy: -self.container.frame.minY) + self.isDraggingHeader = location.y < 66.0 } private func applyDismissProgress(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { @@ -277,10 +283,20 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes let transition: ContainedViewLayoutTransition let dismissProgress: CGFloat if (velocity.y < -0.5 || progress >= 0.5) && self.checkInteractiveDismissWithControllers() { - dismissProgress = 1.0 - targetOffset = 0.0 - transition = .animated(duration: duration, curve: .easeInOut) - self.isDismissed = true + if let controller = self.container.controllers.last as? MinimizableController { + dismissProgress = 0.0 + targetOffset = 0.0 + transition = .immediate + + let topEdgeOffset = self.container.view.convert(self.container.bounds, to: self.view).minY + controller.requestMinimize(topEdgeOffset: topEdgeOffset, initialVelocity: velocity.y) + self.dim.removeFromSupernode() + } else { + dismissProgress = 1.0 + targetOffset = 0.0 + transition = .animated(duration: duration, curve: .easeInOut) + self.isDismissed = true + } } else { dismissProgress = 0.0 targetOffset = self.bounds.height @@ -485,7 +501,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) alphaTransition.updateAlpha(node: self.dim, alpha: 0.0, beginWithCurrentState: true) - if let lastController = self.container.controllers.last, lastController.isMinimized { + if let lastController = self.container.controllers.last as? MinimizableController, lastController.isMinimized { self.dim.layer.removeAllAnimations() } positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: self.bounds.height + self.container.bounds.height / 2.0 + self.bounds.height), beginWithCurrentState: true, completion: { [weak self] _ in diff --git a/submodules/Display/Source/PortalSourceView.swift b/submodules/Display/Source/PortalSourceView.swift index 337edb299f1..3c932b19248 100644 --- a/submodules/Display/Source/PortalSourceView.swift +++ b/submodules/Display/Source/PortalSourceView.swift @@ -47,6 +47,13 @@ open class PortalSourceView: UIView { } } + public func removePortal(view: PortalView) { + if let index = self.portalReferences.firstIndex(where: { $0.portalView === view }) { + self.portalReferences.remove(at: index) + } + view.disablePortal() + } + func setGlobalPortal(view: GlobalPortalView?) { if let globalPortalView = self.globalPortalView { self.globalPortalView = nil diff --git a/submodules/Display/Source/PortalView.swift b/submodules/Display/Source/PortalView.swift index c26d2632fe5..f5583a4cb22 100644 --- a/submodules/Display/Source/PortalView.swift +++ b/submodules/Display/Source/PortalView.swift @@ -23,6 +23,11 @@ public class PortalView { } } + func disablePortal() { + self.view.sourceView = nil + self.sourceView = nil + } + public func reloadPortal() { if let sourceView = self.sourceView as? PortalSourceView { self.reloadPortal(sourceView: sourceView) diff --git a/submodules/Display/Source/UIKitUtils.swift b/submodules/Display/Source/UIKitUtils.swift index 0ec54265fa8..eec308359e9 100644 --- a/submodules/Display/Source/UIKitUtils.swift +++ b/submodules/Display/Source/UIKitUtils.swift @@ -813,6 +813,16 @@ public extension CALayer { } } +public extension CAEmitterCell { + static func createEmitterBehavior(type: String) -> NSObject { + let selector = ["behaviorWith", "Type:"].joined(separator: "") + let behaviorClass = NSClassFromString(["CA", "Emitter", "Behavior"].joined(separator: "")) as! NSObject.Type + let behaviorWithType = behaviorClass.method(for: NSSelectorFromString(selector))! + let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self) + return castedBehaviorWithType(behaviorClass, NSSelectorFromString(selector), type) + } +} + public extension CALayer { func snapshotContentTreeAsView(unhide: Bool = false) -> UIView? { let wasHidden = self.isHidden diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 4ed52f8c825..520910e25e3 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -65,6 +65,7 @@ public enum ViewControllerNavigationPresentation { case flatModal case standaloneModal case modalInLargeLayout + case modalInCompactLayout } public enum TabBarItemContextActionType { @@ -229,11 +230,7 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { } private var navigationBarOrigin: CGFloat = 0.0 - - public var minimizedTopEdgeOffset: CGFloat? - public var minimizedBounds: CGRect? - open var isMinimized: Bool = false - + open var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? { return nil } diff --git a/submodules/DrawingUI/BUILD b/submodules/DrawingUI/BUILD index 4a527421b97..75b58029c90 100644 --- a/submodules/DrawingUI/BUILD +++ b/submodules/DrawingUI/BUILD @@ -97,6 +97,7 @@ swift_library( "//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState", "//submodules/StickerPackPreviewUI:StickerPackPreviewUI", "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/LottieComponentResourceContent", "//submodules/ImageTransparency", "//submodules/GalleryUI", "//submodules/MediaPlayer:UniversalMediaPlayer", diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 908fc048835..e50f772e3ad 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -30,6 +30,8 @@ private func makeEntityView(context: AccountContext, entity: DrawingEntity) -> D return DrawingLocationEntityView(context: context, entity: entity) } else if let entity = entity as? DrawingLinkEntity { return DrawingLinkEntityView(context: context, entity: entity) + } else if let entity = entity as? DrawingWeatherEntity { + return DrawingWeatherEntityView(context: context, entity: entity) } else { return nil } @@ -59,6 +61,9 @@ private func prepareForRendering(entityView: DrawingEntityView) { if let entityView = entityView as? DrawingLinkEntityView { entityView.entity.renderImage = entityView.getRenderImage() } + if let entityView = entityView as? DrawingWeatherEntityView { + entityView.entity.renderImage = entityView.getRenderImage() + } } public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { @@ -397,6 +402,14 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { location.width = floor(self.size.width * 0.85) location.scale = zoomScale } + } else if let weather = entity as? DrawingWeatherEntity { + weather.position = center + if setup { + weather.rotation = rotation + weather.referenceDrawingSize = self.size + weather.width = floor(self.size.width * 0.85) + weather.scale = zoomScale + } } } diff --git a/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift b/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift index 22d81cd41d4..19eeba7632e 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() + string = self.linkEntity.url.uppercased().replacingOccurrences(of: "http://", with: "").replacingOccurrences(of: "https://", with: "") } let text = NSMutableAttributedString(string: string) let range = NSMakeRange(0, text.length) diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 43c4f710c80..ca595aadc51 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -3089,6 +3089,7 @@ public final class DrawingToolsInteraction { var isAdditional = false var isMessage = false var isLink = false + var isWeather = false if let entity = entityView.entity as? DrawingStickerEntity { if case let .dualVideoReference(isAdditionalValue) = entity.content { isVideo = true @@ -3098,6 +3099,8 @@ public final class DrawingToolsInteraction { } } else if entityView.entity is DrawingLinkEntity { isLink = true + } else if entityView.entity is DrawingWeatherEntity { + isWeather = true } guard (!isVideo || isAdditional) && (!isMessage || !isTopmost) else { @@ -3143,7 +3146,7 @@ public final class DrawingToolsInteraction { } })) } - if !isVideo && !isMessage && !isLink { + if !isVideo && !isMessage && !isLink && !isWeather { if let stickerEntity = entityView.entity as? DrawingStickerEntity, case let .file(_, type) = stickerEntity.content, case .reaction = type { } else { @@ -3209,10 +3212,12 @@ public final class DrawingToolsInteraction { self.isActive = false } - public func insertEntity(_ entity: DrawingEntity, scale: CGFloat? = nil, position: CGPoint? = nil) { + public func insertEntity(_ entity: DrawingEntity, scale: CGFloat? = nil, position: CGPoint? = nil, select: Bool = true) { self.entitiesView.prepareNewEntity(entity, scale: scale, position: position) self.entitiesView.add(entity) - self.entitiesView.selectEntity(entity, animate: !(entity is DrawingTextEntity)) + if select { + self.entitiesView.selectEntity(entity, animate: !(entity is DrawingTextEntity)) + } if let entityView = self.entitiesView.getView(for: entity.uuid) { if let textEntityView = entityView as? DrawingTextEntityView { diff --git a/submodules/DrawingUI/Sources/DrawingTextEntityView.swift b/submodules/DrawingUI/Sources/DrawingTextEntityView.swift index 4e9b833497f..ce68e000e17 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntityView.swift @@ -624,6 +624,15 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate } func getRenderSubEntities() -> [DrawingEntity] { + var explicitlyStaticStickers = Set() + if let customEmojiContainerView = self.customEmojiContainerView { + for (key, view) in customEmojiContainerView.emojiLayers { + if let view = view as? EmojiTextAttachmentView, let numFrames = view.contentLayer.numFrames, numFrames == 1 { + explicitlyStaticStickers.insert(key.id) + } + } + } + let textSize = self.textView.bounds.size let textPosition = self.textEntity.position let scale = self.textEntity.scale @@ -638,6 +647,9 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate let emojiTextPosition = emojiRect.center.offsetBy(dx: -textSize.width / 2.0, dy: -textSize.height / 2.0) let entity = DrawingStickerEntity(content: .file(.standalone(media: file), .sticker)) + if explicitlyStaticStickers.contains(file.fileId.id) { + entity.isExplicitlyStatic = true + } entity.referenceDrawingSize = CGSize(width: itemSize * 4.0, height: itemSize * 4.0) entity.scale = scale entity.position = textPosition.offsetBy( diff --git a/submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift b/submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift new file mode 100644 index 00000000000..5815a10143d --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift @@ -0,0 +1,587 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import AccountContext +import TelegramCore +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import StickerResources +import MediaEditor +import TelegramStringFormatting +import LottieComponent +import LottieComponentResourceContent + +private func generateIcon(style: DrawingWeatherEntity.Style) -> UIImage? { + guard let image = UIImage(bundleImageName: "Chat/Attach Menu/Location") else { + return nil + } + return generateImage(image.size, contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + if let cgImage = image.cgImage { + context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) + } + if [.black, .white].contains(style) { + let green: UIColor + let blue: UIColor + + if case .black = style { + green = UIColor(rgb: 0x3EF588) + blue = UIColor(rgb: 0x4FAAFF) + } else { + green = UIColor(rgb: 0x1EBD5E) + blue = UIColor(rgb: 0x1C92FF) + } + + var locations: [CGFloat] = [0.0, 1.0] + let colorsArray = [green.cgColor, blue.cgColor] as NSArray + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions()) + } else { + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + } + }) +} + +public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelegate { + private var weatherEntity: DrawingWeatherEntity { + return self.entity as! DrawingWeatherEntity + } + + let backgroundView: UIView + + let textView: DrawingTextView + + private var animation = ComponentView() + + private var didSetUpAnimationNode = false + private let stickerFetchedDisposable = MetaDisposable() + private let cachedDisposable = MetaDisposable() + + let temperature: String + + init(context: AccountContext, entity: DrawingWeatherEntity) { + self.temperature = stringForTemperature(entity.temperature) + + self.backgroundView = UIView() + self.backgroundView.clipsToBounds = true + + self.textView = DrawingTextView(frame: .zero) + self.textView.clipsToBounds = false + + self.textView.backgroundColor = .clear + self.textView.isEditable = false + self.textView.isSelectable = false + self.textView.contentInset = .zero + self.textView.showsHorizontalScrollIndicator = false + self.textView.showsVerticalScrollIndicator = false + self.textView.scrollsToTop = false + self.textView.isScrollEnabled = false + self.textView.textContainerInset = .zero + self.textView.minimumZoomScale = 1.0 + self.textView.maximumZoomScale = 1.0 + self.textView.keyboardAppearance = .dark + self.textView.autocorrectionType = .default + self.textView.spellCheckingType = .no + self.textView.textContainer.maximumNumberOfLines = 2 + self.textView.textContainer.lineBreakMode = .byTruncatingTail + + super.init(context: context, entity: entity) + + self.textView.delegate = self + self.addSubview(self.backgroundView) + self.addSubview(self.textView) + + self.update(animated: false) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var textSize: CGSize = .zero + public override func sizeThatFits(_ size: CGSize) -> CGSize { + var result = self.textView.sizeThatFits(CGSize(width: self.weatherEntity.width, height: .greatestFiniteMagnitude)) + self.textSize = result + + let widthExtension: CGFloat = result.height * 0.7 + result.width = floorToScreenPixels(max(224.0, ceil(result.width) + 20.0) + widthExtension) + result.height = ceil(result.height * 1.2); + return result; + } + + public override func sizeToFit() { + let center = self.center + let transform = self.transform + self.transform = .identity + super.sizeToFit() + self.center = center + self.transform = transform + } + + public override func layoutSubviews() { + super.layoutSubviews() + + let iconSize = min(80.0, floor(self.bounds.height * 0.7)) + let iconOffset: CGFloat = 0.3 + + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconSize * iconOffset), y: floorToScreenPixels((self.bounds.height - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize)) + + if let icon = self.weatherEntity.icon { + let _ = self.animation.update( + transition: .immediate, + component: AnyComponent( + LottieComponent( + content: LottieComponent.ResourceContent( + context: self.context, + file: icon, + attemptSynchronously: true, + providesPlaceholder: true + ), + color: nil, + placeholderColor: UIColor(rgb: 0x000000, alpha: 0.1), + loop: !["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"].contains(self.weatherEntity.emoji) + ) + ), + environment: {}, + containerSize: iconFrame.size + ) + if let animationView = self.animation.view { + if animationView.superview == nil { + self.addSubview(animationView) + } + animationView.frame = iconFrame + } + } + + self.textView.frame = CGRect(origin: CGPoint(x: self.bounds.width - self.textSize.width - 6.0, y: floorToScreenPixels((self.bounds.height - self.textSize.height) / 2.0)), size: self.textSize) + self.backgroundView.frame = self.bounds + } + + override func selectedTapAction() -> Bool { + let values = [self.entity.scale, self.entity.scale * 0.93, self.entity.scale] + let keyTimes = [0.0, 0.33, 1.0] + self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.3, keyPath: "transform.scale") + + let updatedStyle: DrawingWeatherEntity.Style + switch self.weatherEntity.style { + case .white: + updatedStyle = .black + case .black: + updatedStyle = .transparent + case .transparent: + if self.weatherEntity.hasCustomColor { + updatedStyle = .custom + } else { + updatedStyle = .white + } + case .custom: + updatedStyle = .white + } + self.weatherEntity.style = updatedStyle + + self.update() + + return true + } + + private var displayFontSize: CGFloat { + var textFontSize: CGFloat = 0.07 + let textLength = self.temperature.count + if textLength > 10 { + textFontSize = max(0.01, 0.07 - CGFloat(textLength - 10) / 100.0) + } + + let minFontSize = max(10.0, max(self.weatherEntity.referenceDrawingSize.width, self.weatherEntity.referenceDrawingSize.height) * 0.025) + let maxFontSize = max(10.0, max(self.weatherEntity.referenceDrawingSize.width, self.weatherEntity.referenceDrawingSize.height) * 0.25) + let fontSize = minFontSize + (maxFontSize - minFontSize) * textFontSize + return fontSize + } + + private func updateText() { + let text = NSMutableAttributedString(string: self.temperature.uppercased()) + let range = NSMakeRange(0, text.length) + let fontSize = self.displayFontSize + + self.textView.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(fontSize * 0.24) + + let font = Font.with(size: fontSize, design: .camera, weight: .semibold) + text.addAttribute(.font, value: font, range: range) + text.addAttribute(.kern, value: -3.5 as NSNumber, range: range) + self.textView.font = font + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .left + text.addAttribute(.paragraphStyle, value: paragraphStyle, range: range) + + let textColor: UIColor + switch self.weatherEntity.style { + case .white: + textColor = .black + case .black, .transparent: + textColor = .white + case .custom: + let color = self.weatherEntity.color.toUIColor() + if color.lightness > 0.705 { + textColor = .black + } else { + textColor = .white + } + } + + text.addAttribute(.foregroundColor, value: textColor, range: range) + + self.textView.attributedText = text + self.textView.visualText = text + } + + private var currentStyle: DrawingWeatherEntity.Style? + public override func update(animated: Bool = false) { + self.center = self.weatherEntity.position + self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.weatherEntity.rotation), self.weatherEntity.scale, self.weatherEntity.scale) + + self.textView.frameInsets = UIEdgeInsets(top: 0.15, left: 0.0, bottom: 0.15, right: 0.0) + switch self.weatherEntity.style { + case .white: + self.textView.textColor = .black + self.backgroundView.backgroundColor = .white + self.backgroundView.isHidden = false + case .black: + self.textView.textColor = .white + self.backgroundView.backgroundColor = .black + self.backgroundView.isHidden = false + case .transparent: + self.textView.textColor = .white + self.backgroundView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2) + self.backgroundView.isHidden = false + case .custom: + let color = self.weatherEntity.color.toUIColor() + let textColor: UIColor + if color.lightness > 0.705 { + textColor = .black + } else { + textColor = .white + } + self.textView.textColor = textColor + self.backgroundView.backgroundColor = color + self.backgroundView.isHidden = false + } + self.textView.textAlignment = .left + + self.updateText() + + self.sizeToFit() + + + self.currentStyle = self.weatherEntity.style + + self.backgroundView.layer.cornerRadius = self.textSize.height * 0.2 + if #available(iOS 13.0, *) { + self.backgroundView.layer.cornerCurve = .continuous + } + + super.update(animated: animated) + } + + override func updateSelectionView() { + guard let selectionView = self.selectionView as? DrawingWeatherEntitySelectionView else { + return + } + self.pushIdentityTransformForMeasurement() + + selectionView.transform = .identity + let bounds = self.selectionBounds + let center = bounds.center + + let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0 + selectionView.center = self.convert(center, to: selectionView.superview) + + selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.weatherEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.weatherEntity.scale) * scale + selectionView.selectionInset * 2.0)) + selectionView.transform = CGAffineTransformMakeRotation(self.weatherEntity.rotation) + + self.popIdentityTransformForMeasurement() + } + + override func makeSelectionView() -> DrawingEntitySelectionView? { + if let selectionView = self.selectionView { + return selectionView + } + let selectionView = DrawingWeatherEntitySelectionView() + selectionView.entityView = self + return selectionView + } + + func getRenderImage() -> UIImage? { + let rect = self.bounds + UIGraphicsBeginImageContextWithOptions(rect.size, false, 2.0) + self.drawHierarchy(in: rect, afterScreenUpdates: true) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } + + func getRenderSubEntities() -> [DrawingEntity] { + return [] + } +} + +final class DrawingWeatherEntitySelectionView: DrawingEntitySelectionView { + private let border = SimpleShapeLayer() + private let leftHandle = SimpleShapeLayer() + private let rightHandle = SimpleShapeLayer() + + private var longPressGestureRecognizer: UILongPressGestureRecognizer? + + override init(frame: CGRect) { + let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize) + let handles = [ + self.leftHandle, + self.rightHandle + ] + + super.init(frame: frame) + + self.backgroundColor = .clear + self.isOpaque = false + + self.border.lineCap = .round + self.border.fillColor = UIColor.clear.cgColor + self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.75).cgColor + self.layer.addSublayer(self.border) + + for handle in handles { + handle.bounds = handleBounds + handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor + handle.strokeColor = UIColor(rgb: 0xffffff).cgColor + handle.rasterizationScale = UIScreen.main.scale + handle.shouldRasterize = true + + self.layer.addSublayer(handle) + } + + self.snapTool.onSnapUpdated = { [weak self] type, snapped in + if let self, let entityView = self.entityView { + entityView.onSnapUpdated(type, snapped) + } + } + + let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:))) + self.addGestureRecognizer(longPressGestureRecognizer) + self.longPressGestureRecognizer = longPressGestureRecognizer + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var scale: CGFloat = 1.0 { + didSet { + self.setNeedsLayout() + } + } + + override var selectionInset: CGFloat { + return 15.0 + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + private let snapTool = DrawingEntitySnapTool() + + @objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { + if case .began = gestureRecognizer.state { + self.longPressed() + } + } + + private var currentHandle: CALayer? + override func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingWeatherEntity else { + return + } + let location = gestureRecognizer.location(in: self) + switch gestureRecognizer.state { + case .began: + self.tapGestureRecognizer?.isEnabled = false + self.tapGestureRecognizer?.isEnabled = true + + self.longPressGestureRecognizer?.isEnabled = false + self.longPressGestureRecognizer?.isEnabled = true + + self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position) + + if let sublayers = self.layer.sublayers { + for layer in sublayers { + if layer.frame.contains(location) { + self.currentHandle = layer + self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + entityView.onInteractionUpdated(true) + return + } + } + } + self.currentHandle = self.layer + entityView.onInteractionUpdated(true) + case .changed: + if self.currentHandle == nil { + self.currentHandle = self.layer + } + + let delta = gestureRecognizer.translation(in: entityView.superview) + let parentLocation = gestureRecognizer.location(in: self.superview) + let velocity = gestureRecognizer.velocity(in: entityView.superview) + + var updatedScale = entity.scale + var updatedPosition = entity.position + var updatedRotation = entity.rotation + + if self.currentHandle === self.leftHandle || self.currentHandle === self.rightHandle { + if gestureRecognizer.numberOfTouches > 1 { + return + } + var deltaX = gestureRecognizer.translation(in: self).x + if self.currentHandle === self.leftHandle { + deltaX *= -1.0 + } + let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width + updatedScale = max(0.01, updatedScale * scaleDelta) + + let newAngle: CGFloat + if self.currentHandle === self.leftHandle { + newAngle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x) + } else { + newAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x) + } + var delta = newAngle - updatedRotation + if delta < -.pi { + delta = 2.0 * .pi + delta + } + let velocityValue = sqrt(velocity.x * velocity.x + velocity.y * velocity.y) / 1000.0 + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocityValue, delta: delta, updatedRotation: newAngle, skipMultiplier: 1.0) + } else if self.currentHandle === self.layer { + updatedPosition.x += delta.x + updatedPosition.y += delta.y + + updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size) + } + + entity.scale = updatedScale + entity.position = updatedPosition + entity.rotation = updatedRotation + entityView.update() + + gestureRecognizer.setTranslation(.zero, in: entityView) + case .ended, .cancelled: + self.snapTool.reset() + if self.currentHandle != nil { + self.snapTool.rotationReset() + } + entityView.onInteractionUpdated(false) + default: + break + } + + entityView.onPositionUpdated(entity.position) + } + + override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + guard let entityView = self.entityView as? DrawingWeatherEntityView, let entity = entityView.entity as? DrawingWeatherEntity else { + return + } + + switch gestureRecognizer.state { + case .began, .changed: + if case .began = gestureRecognizer.state { + entityView.onInteractionUpdated(true) + } + let scale = gestureRecognizer.scale + entity.scale = max(0.1, entity.scale * scale) + entityView.update() + + gestureRecognizer.scale = 1.0 + case .ended, .cancelled: + entityView.onInteractionUpdated(false) + default: + break + } + } + + override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + guard let entityView = self.entityView as? DrawingWeatherEntityView, let entity = entityView.entity as? DrawingWeatherEntity else { + return + } + + let velocity = gestureRecognizer.velocity + var updatedRotation = entity.rotation + var rotation: CGFloat = 0.0 + + switch gestureRecognizer.state { + case .began: + self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + entityView.onInteractionUpdated(true) + case .changed: + rotation = gestureRecognizer.rotation + updatedRotation += rotation + + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) + entity.rotation = updatedRotation + entityView.update() + + gestureRecognizer.rotation = 0.0 + case .ended, .cancelled: + self.snapTool.rotationReset() + entityView.onInteractionUpdated(false) + default: + break + } + + entityView.onPositionUpdated(entity.position) + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point) + } + + override func layoutSubviews() { + let inset = self.selectionInset - 10.0 + + let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale)) + let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale) + let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil) + let lineWidth = (1.0 + UIScreenPixel) / self.scale + + let handles = [ + self.leftHandle, + self.rightHandle + ] + + for handle in handles { + handle.path = handlePath + handle.bounds = bounds + handle.lineWidth = lineWidth + } + + self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY) + self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY) + + let width: CGFloat = self.bounds.width - inset * 2.0 + let height: CGFloat = self.bounds.height - inset * 2.0 + let cornerRadius: CGFloat = 12.0 - self.scale + + let perimeter: CGFloat = 2.0 * (width + height - cornerRadius * (4.0 - .pi)) + let count = 12 + let relativeDashLength: CGFloat = 0.25 + let dashLength = perimeter / CGFloat(count) + self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber] + + self.border.lineWidth = 2.0 / self.scale + self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: width, height: height)), cornerRadius: cornerRadius).cgPath + } +} diff --git a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegBinding.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegBinding.h index 5ed187b5fb1..21801d93550 100644 --- a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegBinding.h +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegBinding.h @@ -10,4 +10,5 @@ #import #import #import +#import #import diff --git a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegLiveMuxer.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegLiveMuxer.h new file mode 100644 index 00000000000..ec480c1f139 --- /dev/null +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegLiveMuxer.h @@ -0,0 +1,11 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FFMpegLiveMuxer : NSObject + ++ (bool)remux:(NSString * _Nonnull)path to:(NSString * _Nonnull)outPath offsetSeconds:(double)offsetSeconds; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/FFMpegBinding/Sources/FFMpegLiveMuxer.m b/submodules/FFMpegBinding/Sources/FFMpegLiveMuxer.m new file mode 100644 index 00000000000..85ddce8a9e6 --- /dev/null +++ b/submodules/FFMpegBinding/Sources/FFMpegLiveMuxer.m @@ -0,0 +1,185 @@ +#import +#import + +#include "libavutil/timestamp.h" +#include "libavformat/avformat.h" +#include "libavcodec/avcodec.h" +#include "libswresample/swresample.h" + +#define MOV_TIMESCALE 1000 + +@implementation FFMpegLiveMuxer + ++ (bool)remux:(NSString * _Nonnull)path to:(NSString * _Nonnull)outPath offsetSeconds:(double)offsetSeconds { + AVFormatContext *input_format_context = NULL, *output_format_context = NULL; + AVPacket packet; + const char *in_filename, *out_filename; + int ret, i; + int stream_index = 0; + int *streams_list = NULL; + int number_of_streams = 0; + + in_filename = [path UTF8String]; + out_filename = [outPath UTF8String]; + + if ((ret = avformat_open_input(&input_format_context, in_filename, av_find_input_format("mp4"), NULL)) < 0) { + fprintf(stderr, "Could not open input file '%s'\n", in_filename); + goto end; + } + if ((ret = avformat_find_stream_info(input_format_context, NULL)) < 0) { + fprintf(stderr, "Failed to retrieve input stream information\n"); + goto end; + } + + avformat_alloc_output_context2(&output_format_context, NULL, "mpegts", out_filename); + + if (!output_format_context) { + fprintf(stderr, "Could not create output context\n"); + ret = AVERROR_UNKNOWN; + goto end; + } + + number_of_streams = input_format_context->nb_streams; + streams_list = av_malloc_array(number_of_streams, sizeof(*streams_list)); + + if (!streams_list) { + ret = AVERROR(ENOMEM); + goto end; + } + + for (i = 0; i < input_format_context->nb_streams; i++) { + AVStream *out_stream; + AVStream *in_stream = input_format_context->streams[i]; + AVCodecParameters *in_codecpar = in_stream->codecpar; + + if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO && in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO) { + streams_list[i] = -1; + continue; + } + + streams_list[i] = stream_index++; + + if (in_codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + out_stream = avformat_new_stream(output_format_context, NULL); + if (!out_stream) { + fprintf(stderr, "Failed allocating output stream\n"); + ret = AVERROR_UNKNOWN; + goto end; + } + ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar); + if (ret < 0) { + fprintf(stderr, "Failed to copy codec parameters\n"); + goto end; + } + out_stream->time_base = in_stream->time_base; + out_stream->duration = in_stream->duration; + } else if (in_codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + if (in_codecpar->codec_id != AV_CODEC_ID_AAC) { + streams_list[i] = -1; + continue; + } + + out_stream = avformat_new_stream(output_format_context, NULL); + if (!out_stream) { + fprintf(stderr, "Failed allocating output stream\n"); + ret = AVERROR_UNKNOWN; + goto end; + } + ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar); + if (ret < 0) { + fprintf(stderr, "Failed to copy codec parameters\n"); + goto end; + } + out_stream->time_base = in_stream->time_base; + out_stream->duration = in_stream->duration; + } + } + + if (!(output_format_context->oformat->flags & AVFMT_NOFILE)) { + ret = avio_open(&output_format_context->pb, out_filename, AVIO_FLAG_WRITE); + if (ret < 0) { + fprintf(stderr, "Could not open output file '%s'\n", out_filename); + goto end; + } + } + + AVDictionary* opts = NULL; + ret = avformat_write_header(output_format_context, &opts); + if (ret < 0) { + fprintf(stderr, "Error occurred when opening output file\n"); + goto end; + } + + while (1) { + AVStream *in_stream, *out_stream; + ret = av_read_frame(input_format_context, &packet); + if (ret < 0) + break; + + in_stream = input_format_context->streams[packet.stream_index]; + if (packet.stream_index >= number_of_streams || streams_list[packet.stream_index] < 0) { + av_packet_unref(&packet); + continue; + } + + packet.stream_index = streams_list[packet.stream_index]; + out_stream = output_format_context->streams[packet.stream_index]; + + if (in_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); + packet.dts = av_rescale_q_rnd(packet.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); + packet.pts += (int64_t)(offsetSeconds * out_stream->time_base.den); + packet.dts += (int64_t)(offsetSeconds * out_stream->time_base.den); + packet.duration = av_rescale_q(packet.duration, in_stream->time_base, out_stream->time_base); + packet.pos = -1; + + ret = av_interleaved_write_frame(output_format_context, &packet); + if (ret < 0) { + fprintf(stderr, "Error muxing packet\n"); + av_packet_unref(&packet); + break; + } + } else { + packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); + packet.dts = av_rescale_q_rnd(packet.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); + packet.pts += (int64_t)(offsetSeconds * out_stream->time_base.den); + packet.dts += (int64_t)(offsetSeconds * out_stream->time_base.den); + packet.duration = av_rescale_q(packet.duration, in_stream->time_base, out_stream->time_base); + packet.pos = -1; + + ret = av_interleaved_write_frame(output_format_context, &packet); + if (ret < 0) { + fprintf(stderr, "Error muxing packet\n"); + av_packet_unref(&packet); + break; + } + } + + av_packet_unref(&packet); + } + + av_write_trailer(output_format_context); + +end: + if (input_format_context) { + avformat_close_input(&input_format_context); + } + if (output_format_context && !(output_format_context->oformat->flags & AVFMT_NOFILE)) { + avio_closep(&output_format_context->pb); + } + if (output_format_context) { + avformat_free_context(output_format_context); + } + if (streams_list) { + av_freep(&streams_list); + } + if (ret < 0 && ret != AVERROR_EOF) { + fprintf(stderr, "Error occurred: %s\n", av_err2str(ret)); + return false; + } + + //printf("Remuxed video into %s\n", outPath.UTF8String); + return true; +} + +@end diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 6beea637a0f..c4bbe079186 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -836,7 +836,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } else if let media = media as? TelegramMediaFile, !media.isAnimated { for attribute in media.attributes { switch attribute { - case let .Video(_, dimensions, _, _): + case let .Video(_, dimensions, _, _, _): isVideo = true if dimensions.height > 0 { if CGFloat(dimensions.width) / CGFloat(dimensions.height) > 1.33 { diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index 5393185f882..c0f1930656f 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -512,6 +512,10 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture } } else { self.scrollView.setContentOffset(CGPoint(x: 0.0, y: self.scrollView.contentSize.height / 3.0), animated: true) + + if let chatController = self.baseNavigationController()?.topViewController as? ChatController { + chatController.updatePushedTransition(1.0, transition: .animated(duration: 0.45, curve: .customSpring(damping: 180.0, initialVelocity: 0.0))) + } } } diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 9c39a5d896f..edfca6bc771 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -532,59 +532,78 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } private func contextMenuMainItems() -> Signal<[ContextMenuItem], NoError> { - var items: [ContextMenuItem] = [] - + let peer: Signal if let message = self.message { - let context = self.context - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in - - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let strongSelf = self, let peer = peer else { - return - } - if let navigationController = strongSelf.baseNavigationController() { - strongSelf.beginCustomDismiss(true) + peer = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId)) + } else { + peer = .single(nil) + } + + let context = self.context + return peer + |> map { [weak self] peer -> [ContextMenuItem] in + guard let self else { + return [] + } + var items: [ContextMenuItem] = [] + if let message = self.message { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in + if let self, let peer, let navigationController = self.baseNavigationController() { + self.beginCustomDismiss(true) - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil))) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) Queue.mainQueue().after(0.3) { - strongSelf.completeCustomDismiss() + self.completeCustomDismiss() } } f(.default) - }) - }))) + }))) + + if !message.isCopyProtected() && !self.peerIsCopyProtected && message.paidContent == nil, let media = self.contextAndMedia?.1 { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Gallery_SaveImage, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in + f(.default) + + let _ = (SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: media) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let strongSelf = self else { + return + } + guard let controller = strongSelf.galleryController() else { + return + } + controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_ImageSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + }) + }))) + } + } - if !message.isCopyProtected() && !self.peerIsCopyProtected && message.paidContent == nil, let media = self.contextAndMedia?.1 { - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Gallery_SaveImage, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in - f(.default) - - let _ = (SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: media) - |> deliverOnMainQueue).start(completed: { [weak self] in - guard let strongSelf = self else { - return - } - guard let controller = strongSelf.galleryController() else { - return + if let peer, let message = self.message, canSendMessagesToPeer(peer._asPeer()) { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in + if let self, let navigationController = self.baseNavigationController() { + self.beginCustomDismiss(true) + + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true))) + + Queue.mainQueue().after(0.3) { + self.completeCustomDismiss() } - controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_ImageSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - }) + } + f(.default) }))) } - } - - if self.canDelete() { - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in - f(.default) + + if self.canDelete() { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in + f(.default) - if let strongSelf = self { - strongSelf.footerContentNode.deleteButtonPressed() - } - }))) + if let strongSelf = self { + strongSelf.footerContentNode.deleteButtonPressed() + } + }))) + } + return items } - - return .single(items) } private func openMoreMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) { diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index e7409fa6087..7c82a528e0c 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1235,7 +1235,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } if let file = file { for attribute in file.attributes { - if case let .Video(duration, _, _, _) = attribute, duration >= 30 { + if case let .Video(duration, _, _, _, _) = attribute, duration >= 30 { hintSeekable = true break } @@ -2509,11 +2509,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { guard let videoNode = self.videoNode, let item = self.item else { return .single([]) } + + let peer: Signal + if let (message, _, _) = self.contentInfo() { + peer = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId)) + } else { + peer = .single(nil) + } - return videoNode.status + return combineLatest(queue: Queue.mainQueue(), videoNode.status, peer) |> take(1) - |> deliverOnMainQueue - |> map { [weak self] status -> [ContextMenuItem] in + |> map { [weak self] status, peer -> [ContextMenuItem] in guard let status = status, let strongSelf = self else { return [] } @@ -2552,23 +2558,19 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let (message, _, _) = strongSelf.contentInfo() { let context = strongSelf.context items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in - - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let strongSelf = self, let peer = peer else { - return - } - if let navigationController = strongSelf.baseNavigationController() { - strongSelf.beginCustomDismiss(true) - - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil))) - - Queue.mainQueue().after(0.3) { - strongSelf.completeCustomDismiss() - } + guard let strongSelf = self, let peer = peer else { + return + } + if let navigationController = strongSelf.baseNavigationController() { + strongSelf.beginCustomDismiss(true) + + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) + + Queue.mainQueue().after(0.3) { + strongSelf.completeCustomDismiss() } - f(.default) - }) + } + f(.default) }))) } @@ -2637,6 +2639,22 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } }))) } + + if let peer, let (message, _, _) = strongSelf.contentInfo(), canSendMessagesToPeer(peer._asPeer()) { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in + if let self, let navigationController = self.baseNavigationController() { + self.beginCustomDismiss(true) + + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true))) + + Queue.mainQueue().after(0.3) { + self.completeCustomDismiss() + } + } + f(.default) + }))) + } + if strongSelf.canDelete() { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in f(.default) diff --git a/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift b/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift index cf9bd1260a7..b1738c797fd 100644 --- a/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift +++ b/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift @@ -155,7 +155,7 @@ class BarsComponentController: GeneralChartComponentController { if secondary { var updatedLabels: [LinesChartLabel] = [] for label in labels { - let convertedValue = (Double(label.text) ?? 0.0) * self.conversionRate + let convertedValue = (self.verticalLimitsNumberFormatter.number(from: label.text) as? Double ?? 0.0) * self.conversionRate let text: String if convertedValue > 1.0 { text = String(format: "%0.1f", convertedValue) diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index e2f0c79b932..1b26d6921e9 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -40,7 +40,11 @@ private let productIdentifiers = [ "org.telegram.telegramStars.topup.x750", "org.telegram.telegramStars.topup.x1000", "org.telegram.telegramStars.topup.x1500", - "org.telegram.telegramStars.topup.x2500" + "org.telegram.telegramStars.topup.x2500", + "org.telegram.telegramStars.topup.x5000", + "org.telegram.telegramStars.topup.x10000", + "org.telegram.telegramStars.topup.x25000", + "org.telegram.telegramStars.topup.x35000" ] private extension NSDecimalNumber { @@ -628,6 +632,7 @@ private final class PendingInAppPurchaseState: Codable { case giftCode case giveaway case stars + case starsGift } case subscription @@ -637,6 +642,7 @@ private final class PendingInAppPurchaseState: Codable { case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?) case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32) case stars(count: Int64) + case starsGift(peerId: EnginePeer.Id, count: Int64) public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -670,7 +676,14 @@ private final class PendingInAppPurchaseState: Codable { untilDate: try container.decode(Int32.self, forKey: .untilDate) ) case .stars: - self = .stars(count: try container.decode(Int64.self, forKey: .stars)) + self = .stars( + count: try container.decode(Int64.self, forKey: .stars) + ) + case .starsGift: + self = .starsGift( + peerId: EnginePeer.Id(try container.decode(Int64.self, forKey: .peer)), + count: try container.decode(Int64.self, forKey: .stars) + ) default: throw DecodingError.generic } @@ -706,6 +719,10 @@ private final class PendingInAppPurchaseState: Codable { case let .stars(count): try container.encode(PurposeType.stars.rawValue, forKey: .type) try container.encode(count, forKey: .stars) + case let .starsGift(peerId, count): + try container.encode(PurposeType.starsGift.rawValue, forKey: .type) + try container.encode(peerId.toInt64(), forKey: .peer) + try container.encode(count, forKey: .stars) } } @@ -725,6 +742,8 @@ private final class PendingInAppPurchaseState: Codable { self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate) case let .stars(count, _, _): self = .stars(count: count) + case let .starsGift(peerId, count, _, _): + self = .starsGift(peerId: peerId, count: count) } } @@ -745,6 +764,8 @@ private final class PendingInAppPurchaseState: Codable { return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount) case let .stars(count): return .stars(count: count, currency: currency, amount: amount) + case let .starsGift(peerId, count): + return .starsGift(peerId: peerId, count: count, currency: currency, amount: amount) } } } diff --git a/submodules/InstantPageUI/Sources/InstantPageContentNode.swift b/submodules/InstantPageUI/Sources/InstantPageContentNode.swift index b2c88102099..cf0c2aafbd9 100644 --- a/submodules/InstantPageUI/Sources/InstantPageContentNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageContentNode.swift @@ -319,7 +319,7 @@ public final class InstantPageContentNode : ASDisplayNode { // } } - func updateDetailsExpanded(_ index: Int, _ expanded: Bool, animated: Bool = true, requestLayout: Bool = true) { + public func updateDetailsExpanded(_ index: Int, _ expanded: Bool, animated: Bool = true, requestLayout: Bool = true) { if var currentExpandedDetails = self.currentExpandedDetails { currentExpandedDetails[index] = expanded self.currentExpandedDetails = currentExpandedDetails @@ -353,7 +353,7 @@ public final class InstantPageContentNode : ASDisplayNode { return contentOffset } - func nodeForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsNode? { + public func nodeForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsNode? { for (_, itemNode) in self.visibleItemsWithNodes { if let detailsNode = itemNode as? InstantPageDetailsNode, detailsNode.item === item { return detailsNode diff --git a/submodules/InstantPageUI/Sources/InstantPageController.swift b/submodules/InstantPageUI/Sources/InstantPageController.swift index a17188bf80a..716dc6a464f 100644 --- a/submodules/InstantPageUI/Sources/InstantPageController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageController.swift @@ -8,16 +8,6 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext -public struct InstantPageSourceLocation { - public var userLocation: MediaResourceUserLocation - public var peerType: MediaAutoDownloadPeerType - - public init(userLocation: MediaResourceUserLocation, peerType: MediaAutoDownloadPeerType) { - self.userLocation = userLocation - self.peerType = peerType - } -} - public func instantPageAndAnchor(message: Message) -> (TelegramMediaWebpage, String?)? { for media in message.media { if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index c79d463bd5b..e2e9a15e1e9 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -1428,7 +1428,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { }, openUrl: { _ in }, openPeer: { _ in }, showAll: false) - let peer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", 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) + let peer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", 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 message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peer, text: "", attributes: [], media: [map], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let controller = LocationViewController(context: self.context, subject: EngineMessage(message), params: controllerParams) diff --git a/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift b/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift index 38fff01023a..db6a1f74454 100644 --- a/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift @@ -26,7 +26,7 @@ public final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode { private let buttonNode: HighlightableButtonNode private let arrowNode: InstantPageDetailsArrowNode let separatorNode: ASDisplayNode - let contentNode: InstantPageContentNode + public let contentNode: InstantPageContentNode private let updateExpanded: (Bool) -> Void var expanded: Bool @@ -114,7 +114,7 @@ public final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode { self.updateExpanded(expanded) } - func setExpanded(_ expanded: Bool, animated: Bool) { + public func setExpanded(_ expanded: Bool, animated: Bool) { self.expanded = expanded self.arrowNode.setOpen(expanded, animated: animated) } diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift index a5bf188a886..c33f7e4da77 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift @@ -198,7 +198,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable private let replaceRootController: (ViewController, Promise?) -> Void private let baseNavigationController: NavigationController? - var openUrl: ((InstantPageUrlItem) -> Void)? + public var openUrl: ((InstantPageUrlItem) -> Void)? private var innerOpenUrl: (InstantPageUrlItem) -> Void private var openUrlOptions: (InstantPageUrlItem) -> Void diff --git a/submodules/InstantPageUI/Sources/InstantPageImageItem.swift b/submodules/InstantPageUI/Sources/InstantPageImageItem.swift index 7d3ac039cf9..86bfdbbfdae 100644 --- a/submodules/InstantPageUI/Sources/InstantPageImageItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageImageItem.swift @@ -20,14 +20,14 @@ public final class InstantPageImageItem: InstantPageItem { let webPage: TelegramMediaWebpage - let media: InstantPageMedia + public let media: InstantPageMedia let attributes: [InstantPageImageAttribute] public var medias: [InstantPageMedia] { return [self.media] } - let interactive: Bool + public let interactive: Bool let roundCorners: Bool let fit: Bool diff --git a/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift b/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift index 11484db8b47..c8fcfe96918 100644 --- a/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift +++ b/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift @@ -53,7 +53,7 @@ final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem { } else { return SharedMediaPlaybackData(type: .music, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false, isViewOnce: false)) } - case let .Video(_, _, flags, _): + case let .Video(_, _, flags, _, _): if flags.contains(.instantRoundVideo) { return SharedMediaPlaybackData(type: .instantVideo, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false, isViewOnce: false)) } else { @@ -99,7 +99,7 @@ final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem { return SharedMediaPlaybackDisplayData.music(title: updatedTitle, performer: updatedPerformer, albumArt: albumArt, long: false, caption: nil) } - case let .Video(_, _, flags, _): + case let .Video(_, _, flags, _, _): if flags.contains(.instantRoundVideo) { return SharedMediaPlaybackDisplayData.instantVideo(author: nil, peer: nil, timestamp: 0) } else { @@ -141,30 +141,30 @@ struct InstantPagePlaylistLocation: Equatable, SharedMediaPlaylistLocation { } } -final class InstantPageMediaPlaylist: SharedMediaPlaylist { +public final class InstantPageMediaPlaylist: SharedMediaPlaylist { private let webPage: TelegramMediaWebpage private let items: [InstantPageMedia] private let initialItemIndex: Int - var location: SharedMediaPlaylistLocation { + public var location: SharedMediaPlaylistLocation { return InstantPagePlaylistLocation(webpageId: self.webPage.webpageId) } - var currentItemDisappeared: (() -> Void)? + public var currentItemDisappeared: (() -> Void)? private var currentItem: InstantPageMedia? private var playedToEnd: Bool = false private var order: MusicPlaybackSettingsOrder = .regular - private(set) var looping: MusicPlaybackSettingsLooping = .none + public private(set) var looping: MusicPlaybackSettingsLooping = .none - let id: SharedMediaPlaylistId + public let id: SharedMediaPlaylistId private let stateValue = Promise() - var state: Signal { + public var state: Signal { return self.stateValue.get() } - init(webPage: TelegramMediaWebpage, items: [InstantPageMedia], initialItemIndex: Int) { + public init(webPage: TelegramMediaWebpage, items: [InstantPageMedia], initialItemIndex: Int) { assert(Queue.mainQueue().isCurrent()) self.id = InstantPageMediaPlaylistId(webpageId: webPage.webpageId) @@ -176,7 +176,7 @@ final class InstantPageMediaPlaylist: SharedMediaPlaylist { self.control(.next) } - func control(_ action: SharedMediaPlaylistControlAction) { + public func control(_ action: SharedMediaPlaylistControlAction) { assert(Queue.mainQueue().isCurrent()) switch action { @@ -228,14 +228,14 @@ final class InstantPageMediaPlaylist: SharedMediaPlaylist { } } - func setOrder(_ order: MusicPlaybackSettingsOrder) { + public func setOrder(_ order: MusicPlaybackSettingsOrder) { if self.order != order { self.order = order self.updateState() } } - func setLooping(_ looping: MusicPlaybackSettingsLooping) { + public func setLooping(_ looping: MusicPlaybackSettingsLooping) { if self.looping != looping { self.looping = looping self.updateState() @@ -246,6 +246,6 @@ final class InstantPageMediaPlaylist: SharedMediaPlaylist { self.stateValue.set(.single(SharedMediaPlaylistState(loading: false, playedToEnd: self.playedToEnd, item: self.currentItem.flatMap({ InstantPageMediaPlaylistItem(webPage: self.webPage, item: $0) }), nextItem: nil, previousItem: nil, order: self.order, looping: self.looping))) } - func onItemPlaybackStarted(_ item: SharedMediaPlaylistItem) { + public func onItemPlaybackStarted(_ item: SharedMediaPlaylistItem) { } } diff --git a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift index efb02b214e9..2d265e3e971 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift @@ -46,7 +46,7 @@ private enum JoinState: Equatable { } } -final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { +public final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { private let context: AccountContext let safeInset: CGFloat private let transparent: Bool @@ -197,7 +197,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { self.joinDisposable.dispose() } - func update(strings: PresentationStrings, theme: InstantPageTheme) { + public func update(strings: PresentationStrings, theme: InstantPageTheme) { if self.strings !== strings || self.theme !== theme { let themeUpdated = self.theme !== theme self.strings = strings @@ -206,7 +206,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { } } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { } private func applyThemeAndStrings(themeUpdated: Bool) { @@ -263,7 +263,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { } } - override func layout() { + public override func layout() { super.layout() let size = self.bounds.size @@ -290,14 +290,14 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { } } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + public func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } - func updateHiddenMedia(media: InstantPageMedia?) { + public func updateHiddenMedia(media: InstantPageMedia?) { } - func updateIsVisible(_ isVisible: Bool) { + public func updateIsVisible(_ isVisible: Bool) { } @objc func buttonPressed() { diff --git a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift index 3470e9d9c66..778b7033059 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift @@ -16,7 +16,7 @@ public final class InstantPagePlayableVideoItem: InstantPageItem { return [self.media] } - let interactive: Bool + public let interactive: Bool public let wantsNode: Bool = true public let separatesTiles: Bool = false diff --git a/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift b/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift index e08a437250f..318ecae4ef7 100644 --- a/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift @@ -7,7 +7,7 @@ import SwiftSignalKit import AccountContext import TelegramUIPreferences -final class InstantPageReferenceController: ViewController { +public final class InstantPageReferenceController: ViewController { private var controllerNode: InstantPageReferenceControllerNode { return self.displayNode as! InstantPageReferenceControllerNode } @@ -23,7 +23,7 @@ final class InstantPageReferenceController: ViewController { private let openUrlIn: (InstantPageUrlItem) -> Void private let present: (ViewController, Any?) -> Void - init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { + public init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.sourceLocation = sourceLocation self.theme = theme diff --git a/submodules/InstantPageUI/Sources/InstantPageScrollableNode.swift b/submodules/InstantPageUI/Sources/InstantPageScrollableNode.swift index cf9902a4829..201756251b5 100644 --- a/submodules/InstantPageUI/Sources/InstantPageScrollableNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageScrollableNode.swift @@ -50,11 +50,11 @@ public final class InstantPageScrollableContentNode: ASDisplayNode { } } -final class InstantPageScrollableNode: ASScrollNode, InstantPageNode { - let item: InstantPageScrollableItem +public final class InstantPageScrollableNode: ASScrollNode, InstantPageNode { + public let item: InstantPageScrollableItem let contentNode: InstantPageScrollableContentNode - var contentOffset: CGPoint { + public var contentOffset: CGPoint { return self.view.contentOffset } @@ -90,19 +90,19 @@ final class InstantPageScrollableNode: ASScrollNode, InstantPageNode { } } - func updateIsVisible(_ isVisible: Bool) { + public func updateIsVisible(_ isVisible: Bool) { } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + public func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } - func updateHiddenMedia(media: InstantPageMedia?) { + public func updateHiddenMedia(media: InstantPageMedia?) { } - func update(strings: PresentationStrings, theme: InstantPageTheme) { + public func update(strings: PresentationStrings, theme: InstantPageTheme) { } } diff --git a/submodules/InstantPageUI/Sources/InstantPageTextItem.swift b/submodules/InstantPageUI/Sources/InstantPageTextItem.swift index c01951175ba..af427754de9 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTextItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTextItem.swift @@ -38,10 +38,10 @@ struct InstantPageTextImageItem { let id: EngineMedia.Id } -struct InstantPageTextAnchorItem { - let name: String - let anchorText: NSAttributedString? - let empty: Bool +public struct InstantPageTextAnchorItem { + public let name: String + public let anchorText: NSAttributedString? + public let empty: Bool } public struct InstantPageTextRangeRectEdge: Equatable { @@ -63,7 +63,7 @@ public final class InstantPageTextLine { let strikethroughItems: [InstantPageTextStrikethroughItem] let markedItems: [InstantPageTextMarkedItem] let imageItems: [InstantPageTextImageItem] - let anchorItems: [InstantPageTextAnchorItem] + public let anchorItems: [InstantPageTextAnchorItem] let isRTL: Bool init(line: CTLine, range: NSRange, frame: CGRect, strikethroughItems: [InstantPageTextStrikethroughItem], markedItems: [InstantPageTextMarkedItem], imageItems: [InstantPageTextImageItem], anchorItems: [InstantPageTextAnchorItem], isRTL: Bool) { @@ -96,7 +96,7 @@ public final class InstantPageTextItem: InstantPageItem { let alignment: NSTextAlignment let opaqueBackground: Bool public let medias: [InstantPageMedia] = [] - let anchors: [String: (Int, Bool)] + public let anchors: [String: (Int, Bool)] public let wantsNode: Bool = false public let separatesTiles: Bool = false public var selectable: Bool = true diff --git a/submodules/InstantPageUI/Sources/InstantPageTheme.swift b/submodules/InstantPageUI/Sources/InstantPageTheme.swift index 0a9819a0f4a..3706d23bc54 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTheme.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTheme.swift @@ -84,32 +84,32 @@ public struct InstantPageTextCategories { } public final class InstantPageTheme { - let type: InstantPageThemeType - let pageBackgroundColor: UIColor + public let type: InstantPageThemeType + public let pageBackgroundColor: UIColor - let textCategories: InstantPageTextCategories - let serif: Bool + public let textCategories: InstantPageTextCategories + public let serif: Bool - let codeBlockBackgroundColor: UIColor + public let codeBlockBackgroundColor: UIColor - let linkColor: UIColor - let textHighlightColor: UIColor - let linkHighlightColor: UIColor - let markerColor: UIColor + public let linkColor: UIColor + public let textHighlightColor: UIColor + public let linkHighlightColor: UIColor + public let markerColor: UIColor - let panelBackgroundColor: UIColor - let panelHighlightedBackgroundColor: UIColor - let panelPrimaryColor: UIColor - let panelSecondaryColor: UIColor - let panelAccentColor: UIColor + public let panelBackgroundColor: UIColor + public let panelHighlightedBackgroundColor: UIColor + public let panelPrimaryColor: UIColor + public let panelSecondaryColor: UIColor + public let panelAccentColor: UIColor - let tableBorderColor: UIColor - let tableHeaderColor: UIColor - let controlColor: UIColor + public let tableBorderColor: UIColor + public let tableHeaderColor: UIColor + public let controlColor: UIColor - let imageTintColor: UIColor? + public let imageTintColor: UIColor? - let overlayPanelColor: UIColor + public let overlayPanelColor: UIColor public init(type: InstantPageThemeType, pageBackgroundColor: UIColor, textCategories: InstantPageTextCategories, serif: Bool, codeBlockBackgroundColor: UIColor, linkColor: UIColor, textHighlightColor: UIColor, linkHighlightColor: UIColor, markerColor: UIColor, panelBackgroundColor: UIColor, panelHighlightedBackgroundColor: UIColor, panelPrimaryColor: UIColor, panelSecondaryColor: UIColor, panelAccentColor: UIColor, tableBorderColor: UIColor, tableHeaderColor: UIColor, controlColor: UIColor, imageTintColor: UIColor?, overlayPanelColor: UIColor) { self.type = type @@ -264,6 +264,10 @@ private let darkTheme = InstantPageTheme( private func fontSizeMultiplierForVariant(_ variant: InstantPagePresentationFontSize) -> CGFloat { switch variant { + case .xxsmall: + return 0.5 + case .xsmall: + return 0.75 case .small: return 0.85 case .standard: @@ -271,7 +275,7 @@ private func fontSizeMultiplierForVariant(_ variant: InstantPagePresentationFont case .large: return 1.15 case .xlarge: - return 1.3 + return 1.25 case .xxlarge: return 1.5 } @@ -308,7 +312,7 @@ func instantPageThemeTypeForSettingsAndTime(themeSettings: PresentationThemeSett return (settings.themeType, false) } -func instantPageThemeForType(_ type: InstantPageThemeType, settings: InstantPagePresentationSettings) -> InstantPageTheme { +public func instantPageThemeForType(_ type: InstantPageThemeType, settings: InstantPagePresentationSettings) -> InstantPageTheme { switch type { case .light: return lightTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif) @@ -327,7 +331,7 @@ extension ActionSheetControllerTheme { } } -extension ActionSheetController { +public extension ActionSheetController { convenience init(instantPageTheme: InstantPageTheme) { self.init(theme: ActionSheetControllerTheme(instantPageTheme: instantPageTheme), allowInputInset: false) } diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index f522d8bb2d3..0a88ade9d2d 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -12,14 +12,6 @@ struct ArbitraryRandomNumberGenerator : RandomNumberGenerator { func next() -> UInt64 { return UInt64(drand48() * Double(UInt64.max)) } } -func createEmitterBehavior(type: String) -> NSObject { - let selector = ["behaviorWith", "Type:"].joined(separator: "") - let behaviorClass = NSClassFromString(["CA", "Emitter", "Behavior"].joined(separator: "")) as! NSObject.Type - let behaviorWithType = behaviorClass.method(for: NSSelectorFromString(selector))! - let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self) - return castedBehaviorWithType(behaviorClass, NSSelectorFromString(selector), type) -} - func generateMaskImage(size originalSize: CGSize, position: CGPoint, inverse: Bool) -> UIImage? { var size = originalSize var position = position @@ -123,10 +115,10 @@ public class InvisibleInkDustView: UIView { emitter.setValue(2.0, forKey: "massRange") self.emitter = emitter - let fingerAttractor = createEmitterBehavior(type: "simpleAttractor") + let fingerAttractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor") fingerAttractor.setValue("fingerAttractor", forKey: "name") - let alphaBehavior = createEmitterBehavior(type: "valueOverLife") + let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") alphaBehavior.setValue("color.alpha", forKey: "keyPath") alphaBehavior.setValue([0.0, 0.0, 1.0, 0.0, -1.0], forKey: "values") alphaBehavior.setValue(true, forKey: "additive") @@ -435,10 +427,10 @@ public class InvisibleInkDustNode: ASDisplayNode { emitter.setValue(2.0, forKey: "massRange") self.emitter = emitter - let fingerAttractor = createEmitterBehavior(type: "simpleAttractor") + let fingerAttractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor") fingerAttractor.setValue("fingerAttractor", forKey: "name") - let alphaBehavior = createEmitterBehavior(type: "valueOverLife") + let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") alphaBehavior.setValue("color.alpha", forKey: "keyPath") alphaBehavior.setValue([0.0, 0.0, 1.0, 0.0, -1.0], forKey: "values") alphaBehavior.setValue(true, forKey: "additive") diff --git a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift index d6af2f9e1a2..5e27921837f 100644 --- a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift @@ -40,12 +40,12 @@ public class MediaDustLayer: CALayer { emitter.setValue(0.01, forKey: "massRange") self.emitter = emitter - let alphaBehavior = createEmitterBehavior(type: "valueOverLife") + let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") alphaBehavior.setValue("color.alpha", forKey: "keyPath") alphaBehavior.setValue([0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1], forKey: "values") alphaBehavior.setValue(true, forKey: "additive") - let scaleBehavior = createEmitterBehavior(type: "valueOverLife") + let scaleBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") scaleBehavior.setValue("scale", forKey: "keyPath") scaleBehavior.setValue([0.0, 0.5], forKey: "values") scaleBehavior.setValue([0.0, 0.05], forKey: "locations") @@ -154,31 +154,31 @@ public class MediaDustNode: ASDisplayNode { emitter.setValue(0.01, forKey: "massRange") self.emitter = emitter - let alphaBehavior = createEmitterBehavior(type: "valueOverLife") + let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") alphaBehavior.setValue("color.alpha", forKey: "keyPath") alphaBehavior.setValue([0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1], forKey: "values") alphaBehavior.setValue(true, forKey: "additive") - let scaleBehavior = createEmitterBehavior(type: "valueOverLife") + let scaleBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") scaleBehavior.setValue("scale", forKey: "keyPath") scaleBehavior.setValue([0.0, 0.5], forKey: "values") scaleBehavior.setValue([0.0, 0.05], forKey: "locations") - let randomAttractor0 = createEmitterBehavior(type: "simpleAttractor") + let randomAttractor0 = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor") randomAttractor0.setValue("randomAttractor0", forKey: "name") randomAttractor0.setValue(20, forKey: "falloff") randomAttractor0.setValue(35, forKey: "radius") randomAttractor0.setValue(5, forKey: "stiffness") randomAttractor0.setValue(NSValue(cgPoint: .zero), forKey: "position") - let randomAttractor1 = createEmitterBehavior(type: "simpleAttractor") + let randomAttractor1 = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor") randomAttractor1.setValue("randomAttractor1", forKey: "name") randomAttractor1.setValue(20, forKey: "falloff") randomAttractor1.setValue(35, forKey: "radius") randomAttractor1.setValue(5, forKey: "stiffness") randomAttractor1.setValue(NSValue(cgPoint: .zero), forKey: "position") - let fingerAttractor = createEmitterBehavior(type: "simpleAttractor") + let fingerAttractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor") fingerAttractor.setValue("fingerAttractor", forKey: "name") let behaviors = [randomAttractor0, randomAttractor1, fingerAttractor, alphaBehavior, scaleBehavior] diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index f69aaf31157..d69e30ea2be 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -740,7 +740,7 @@ public final class InviteLinkViewController: ViewController { if requestsState.importers.isEmpty && requestsState.isLoadingMore { count = min(4, state.count) 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) + 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(.request(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, true)) } @@ -774,7 +774,7 @@ public final class InviteLinkViewController: ViewController { if state.importers.isEmpty && state.isLoadingMore { count = min(4, state.count) 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) + 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)) } diff --git a/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift b/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift index 7c467645b29..34a6d5ff2c8 100644 --- a/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift +++ b/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift @@ -449,7 +449,11 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo statusText = "" statusColor = item.presentationData.theme.list.itemPrimaryTextColor } else if let _ = peer.botInfo { - statusText = item.presentationData.strings.Bot_GenericBotStatus + if let subscriberCount = peer.subscriberCount { + statusText = item.presentationData.strings.Conversation_StatusBotSubscribers(subscriberCount) + } else { + statusText = item.presentationData.strings.Bot_GenericBotStatus + } statusColor = item.presentationData.theme.list.itemSecondaryTextColor } else if case .generic = item.mode, !servicePeer, let presence = item.presence { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 diff --git a/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift b/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift index 7382d2117b4..509508353a9 100644 --- a/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift +++ b/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift @@ -10,11 +10,17 @@ import LocationResources import ShimmerEffect public final class ItemListVenueItem: ListViewItem, ItemListItem { + public enum InfoIcon { + case info + case goTo + } + let presentationData: ItemListPresentationData let engine: TelegramEngine let venue: TelegramMediaMap? let title: String? let subtitle: String? + let icon: InfoIcon let style: ItemListStyle let action: (() -> Void)? let infoAction: (() -> Void)? @@ -22,12 +28,13 @@ public final class ItemListVenueItem: ListViewItem, ItemListItem { public let sectionId: ItemListSectionId let header: ListViewItemHeader? - public init(presentationData: ItemListPresentationData, engine: TelegramEngine, venue: TelegramMediaMap?, title: String? = nil, subtitle: String? = nil, sectionId: ItemListSectionId = 0, style: ItemListStyle, action: (() -> Void)?, infoAction: (() -> Void)? = nil, header: ListViewItemHeader? = nil) { + public init(presentationData: ItemListPresentationData, engine: TelegramEngine, venue: TelegramMediaMap?, title: String? = nil, subtitle: String? = nil, icon: ItemListVenueItem.InfoIcon = .info, sectionId: ItemListSectionId = 0, style: ItemListStyle, action: (() -> Void)?, infoAction: (() -> Void)? = nil, header: ListViewItemHeader? = nil) { self.presentationData = presentationData self.engine = engine self.venue = venue self.title = title self.subtitle = subtitle + self.icon = icon self.sectionId = sectionId self.style = style self.action = action @@ -274,7 +281,15 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor - strongSelf.infoButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoIcon"), color: item.presentationData.theme.list.itemAccentColor), for: .normal) + + let iconName: String + switch item.icon { + case .info: + iconName = "Location/InfoIcon" + case .goTo: + iconName = "Location/GoTo" + } + strongSelf.infoButton.setImage(generateTintedImage(image: UIImage(bundleImageName: iconName), color: item.presentationData.theme.list.itemAccentColor), for: .normal) } let transition = ContainedViewLayoutTransition.immediate diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 022e49295ae..8b97f917a76 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -297,7 +297,7 @@ public func legacyEnqueueGifMessage(account: Account, data: Data, correlationId: let finalDimensions = TGMediaVideoConverter.dimensions(for: dimensions, adjustments: nil, preset: TGMediaVideoConversionPresetAnimation) var fileAttributes: [TelegramMediaFileAttribute] = [] - fileAttributes.append(.Video(duration: 0.0, size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil)) + fileAttributes.append(.Video(duration: 0.0, size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil)) fileAttributes.append(.FileName(fileName: fileName)) fileAttributes.append(.Animated) @@ -339,7 +339,7 @@ public func legacyEnqueueVideoMessage(account: Account, data: Data, correlationI let finalDimensions = TGMediaVideoConverter.dimensions(for: dimensions, adjustments: nil, preset: TGMediaVideoConversionPresetAnimation) var fileAttributes: [TelegramMediaFileAttribute] = [] - fileAttributes.append(.Video(duration: 0.0, size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil)) + fileAttributes.append(.Video(duration: 0.0, size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil)) fileAttributes.append(.FileName(fileName: fileName)) fileAttributes.append(.Animated) @@ -981,7 +981,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A if !asFile { // MARK: Nicegram RoundedVideos, change to 'flags: videoFlags' - fileAttributes.append(.Video(duration: finalDuration, size: PixelDimensions(finalDimensions), flags: videoFlags, preloadSize: nil)) + fileAttributes.append(.Video(duration: finalDuration, size: PixelDimensions(finalDimensions), flags: videoFlags, preloadSize: nil, coverTime: nil)) if let adjustments = adjustments { if adjustments.sendAsGif { fileAttributes.append(.Animated) diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index d6dbd050291..79d1597354d 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -477,16 +477,18 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender } func lcm(_ x: Int64, _ y: Int64) -> Int64 { + let x = max(x, 1) + let y = max(y, 1) return x / gcd(x, y) * y } - + return combineLatest(durations) |> map { durations in var result: Double let minDuration: Double = 3.0 if durations.count > 1 { let reduced = durations.reduce(1.0) { lhs, rhs -> Double in - return Double(lcm(Int64(lhs * 10.0), Int64(rhs * 10.0))) + return Double(lcm(Int64(lhs * 100.0), Int64(rhs * 100.0))) } result = min(6.0, Double(reduced) / 10.0) } else if let duration = durations.first { diff --git a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift index 4bbb6d07fa3..de538291375 100644 --- a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift @@ -16,6 +16,7 @@ import UrlWhitelist import AccountContext import TelegramStringFormatting import WallpaperResources +import UrlEscaping private let iconFont = Font.with(size: 30.0, design: .round, weight: .bold) @@ -333,7 +334,21 @@ public final class ListMessageSnippetItemNode: ListMessageNode { mutableDescriptionText.append(NSAttributedString(string: text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)) } - let plainUrlString = NSAttributedString(string: content.url.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor) + var address = content.url + if let components = URLComponents(string: address) { + if #available(iOS 16.0, *), let encodedHost = components.encodedHost { + if let decodedHost = components.host, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } else if let encodedHost = components.host { + if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } + } + address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + + let plainUrlString = NSAttributedString(string: address.replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "tonsite://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor) let urlString = NSMutableAttributedString() urlString.append(plainUrlString) urlString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: content.url, range: NSMakeRange(0, urlString.length)) @@ -397,8 +412,13 @@ public final class ListMessageSnippetItemNode: ListMessageNode { let rawUrlString = urlString var parsedUrl = URL(string: urlString) if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") { - urlString = "http://" + urlString - parsedUrl = URL(string: urlString) + if let mappedURL = URL(string: "https://\(urlString)"), let host = mappedURL.host, host.lowercased().hasSuffix(".ton") { + urlString = "tonsite://" + urlString + parsedUrl = URL(string: urlString) + } else { + urlString = "http://" + urlString + parsedUrl = URL(string: urlString) + } } var host: String? = concealed ? urlString : parsedUrl?.host if host == nil { @@ -432,8 +452,23 @@ public final class ListMessageSnippetItemNode: ListMessageNode { mutableDescriptionText.append(NSAttributedString(string: messageText + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)) } + var address = urlString + if let components = URLComponents(string: address) { + if #available(iOS 16.0, *), let encodedHost = components.encodedHost { + if let decodedHost = components.host, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } else if let encodedHost = components.host { + if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } + } + address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + urlString = address + let urlAttributedString = NSMutableAttributedString() - urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor)) + urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "tonsite://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor)) if item.presentationData.theme.theme.list.itemAccentColor.isEqual(item.presentationData.theme.theme.list.itemPrimaryTextColor) { urlAttributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: NSMakeRange(0, urlAttributedString.length)) } @@ -465,8 +500,13 @@ public final class ListMessageSnippetItemNode: ListMessageNode { let rawUrlString = urlString var parsedUrl = URL(string: urlString) if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") { - urlString = "http://" + urlString - parsedUrl = URL(string: urlString) + if let mappedURL = URL(string: "https://\(urlString)"), let host = mappedURL.host, host.lowercased().hasSuffix(".ton") { + urlString = "tonsite://" + urlString + parsedUrl = URL(string: urlString) + } else { + urlString = "http://" + urlString + parsedUrl = URL(string: urlString) + } } let host: String? = concealed ? urlString : parsedUrl?.host if let url = parsedUrl, let host = host { @@ -487,8 +527,23 @@ public final class ListMessageSnippetItemNode: ListMessageNode { mutableDescriptionText.append(NSAttributedString(string: messageText + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)) } + var address = urlString + if let components = URLComponents(string: address) { + if #available(iOS 16.0, *), let encodedHost = components.encodedHost { + if let decodedHost = components.host, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } else if let encodedHost = components.host { + if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } + } + address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + urlString = address + let urlAttributedString = NSMutableAttributedString() - urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor)) + urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "tonsite://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor)) if item.presentationData.theme.theme.list.itemAccentColor.isEqual(item.presentationData.theme.theme.list.itemPrimaryTextColor) { urlAttributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: NSMakeRange(0, urlAttributedString.length)) } diff --git a/submodules/LocationUI/Sources/LocationMapNode.swift b/submodules/LocationUI/Sources/LocationMapNode.swift index e2aa9886b97..9025e079fd7 100644 --- a/submodules/LocationUI/Sources/LocationMapNode.swift +++ b/submodules/LocationUI/Sources/LocationMapNode.swift @@ -189,6 +189,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { public static let defaultMapSpan = MKCoordinateSpan(latitudeDelta: 0.016, longitudeDelta: 0.016) public static let viewMapSpan = MKCoordinateSpan(latitudeDelta: 0.008, longitudeDelta: 0.008) + public static let globalMapSpan = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1) class ProximityCircleRenderer: MKCircleRenderer { override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) { diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index df4d05e948a..16cb1f95ddb 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -24,7 +24,7 @@ class LocationPickerInteraction { let toggleMapModeSelection: () -> Void let updateMapMode: (LocationMapMode) -> Void let goToUserLocation: () -> Void - let goToCoordinate: (CLLocationCoordinate2D) -> Void + let goToCoordinate: (CLLocationCoordinate2D, Bool) -> Void let openSearch: () -> Void let updateSearchQuery: (String) -> Void let dismissSearch: () -> Void @@ -33,7 +33,7 @@ class LocationPickerInteraction { let openHomeWorkInfo: () -> Void let showPlacesInThisArea: () -> Void - init(sendLocation: @escaping (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) { + init(sendLocation: @escaping (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D, Bool) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) { self.sendLocation = sendLocation self.sendLiveLocation = sendLiveLocation self.sendVenue = sendVenue @@ -88,6 +88,7 @@ public final class LocationPickerController: ViewController, AttachmentContainab public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } + public var isMinimized: Bool = false public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: LocationPickerMode, source: Source = .generic, initialLocation: CLLocationCoordinate2D? = nil, completion: @escaping (TelegramMediaMap, Int64?, String?, String?, String?) -> Void) { self.context = context @@ -230,14 +231,14 @@ public final class LocationPickerController: ViewController, AttachmentContainab return } strongSelf.controllerNode.goToUserLocation() - }, goToCoordinate: { [weak self] coordinate in + }, goToCoordinate: { [weak self] coordinate, zoomOut in guard let strongSelf = self else { return } strongSelf.controllerNode.updateState { state in var state = state state.displayingMapModeOptions = false - state.selectedLocation = .location(coordinate, nil) + state.selectedLocation = .location(coordinate, nil, zoomOut) state.searchingVenuesAround = false return state } @@ -396,44 +397,6 @@ public final class LocationPickerController: ViewController, AttachmentContainab } private final class LocationPickerContext: AttachmentMediaPickerContext { - var selectionCount: Signal { - return .single(0) - } - - var caption: Signal { - return .single(nil) - } - - var hasCaption: Bool { - return false - } - - var captionIsAboveMedia: Signal { - return .single(false) - } - - func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { - } - - public var loadingProgress: Signal { - return .single(nil) - } - - public var mainButtonState: Signal { - return .single(nil) - } - - func setCaption(_ caption: NSAttributedString) { - } - - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { - } - - func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { - } - - func mainButtonAction() { - } } public func storyLocationPickerController( diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index 5e8ff2ca938..77df9cf0d44 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -219,7 +219,7 @@ private func preparedTransition(from fromEntries: [LocationPickerEntry], to toEn enum LocationPickerLocation: Equatable { case none case selecting - case location(CLLocationCoordinate2D, String?) + case location(CLLocationCoordinate2D, String?, Bool) case venue(TelegramMediaMap, Int64?, String?) var isCustom: Bool { @@ -245,8 +245,8 @@ enum LocationPickerLocation: Equatable { } else { return false } - case let .location(lhsCoordinate, lhsAddress): - if case let .location(rhsCoordinate, rhsAddress) = rhs, locationCoordinatesAreEqual(lhsCoordinate, rhsCoordinate), lhsAddress == rhsAddress { + case let .location(lhsCoordinate, lhsAddress, lhsGlobal): + if case let .location(rhsCoordinate, rhsAddress, rhsGlobal) = rhs, locationCoordinatesAreEqual(lhsCoordinate, rhsCoordinate), lhsAddress == rhsAddress, lhsGlobal == rhsGlobal { return true } else { return false @@ -589,7 +589,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM var entries: [LocationPickerEntry] = [] switch state.selectedLocation { - case let .location(coordinate, address): + case let .location(coordinate, address, _): let title: String switch strongSelf.mode { case .share: @@ -722,12 +722,13 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM strongSelf.headerNode.mapNode.resetAnnotationSelection() case .selecting: strongSelf.headerNode.mapNode.resetAnnotationSelection() - case let .location(coordinate, address): + case let .location(coordinate, address, global): var updateMap = false + let span = global ? LocationMapNode.globalMapSpan : LocationMapNode.defaultMapSpan switch previousState.selectedLocation { case .none, .venue: updateMap = true - case let .location(previousCoordinate, _): + case let .location(previousCoordinate, _, _): if !locationCoordinatesAreEqual(previousCoordinate, coordinate) { updateMap = true } @@ -735,7 +736,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM break } if updateMap { - strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, isUserLocation: false, hidePicker: false, animated: true) + strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, span: span, isUserLocation: false, hidePicker: false, animated: true) strongSelf.headerNode.mapNode.switchToPicking(animated: false) } @@ -849,11 +850,11 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM )) } - if case let .location(coordinate, address) = state.selectedLocation, address == nil { + if case let .location(coordinate, address, global) = state.selectedLocation, address == nil { setupGeocoding(coordinate, { [weak self] geoAddress, address, cityName, streetName, countryCode, isStreet in self?.updateState { state in var state = state - state.selectedLocation = .location(coordinate, address) + state.selectedLocation = .location(coordinate, address, global) state.geoAddress = geoAddress state.city = cityName state.street = streetName @@ -938,7 +939,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM strongSelf.updateState { state in var state = state if case .selecting = state.selectedLocation { - state.selectedLocation = .location(coordinate, nil) + state.selectedLocation = .location(coordinate, nil, false) state.searchingVenuesAround = false } return state @@ -1231,7 +1232,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } func requestPlacesAtSelectedLocation() { - if case let .location(coordinate, _) = self.state.selectedLocation { + if case let .location(coordinate, _, _) = self.state.selectedLocation { self.headerNode.mapNode.setMapCenter(coordinate: coordinate, animated: true) self.searchVenuesPromise.set(.single(coordinate)) self.updateState { state in diff --git a/submodules/LocationUI/Sources/LocationSearchContainerNode.swift b/submodules/LocationUI/Sources/LocationSearchContainerNode.swift index 38f7848b769..0749c3fff01 100644 --- a/submodules/LocationUI/Sources/LocationSearchContainerNode.swift +++ b/submodules/LocationUI/Sources/LocationSearchContainerNode.swift @@ -23,6 +23,7 @@ private struct LocationSearchEntry: Identifiable, Comparable { let resultId: String? let title: String? let distance: Double + let story: Bool var stableId: String { return self.location.venue?.id ?? "" @@ -50,6 +51,9 @@ private struct LocationSearchEntry: Identifiable, Comparable { if lhs.distance != rhs.distance { return false } + if lhs.story != rhs.story { + return false + } return true } @@ -57,7 +61,7 @@ private struct LocationSearchEntry: Identifiable, Comparable { return lhs.index < rhs.index } - func item(engine: TelegramEngine, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void) -> ListViewItem { + func item(engine: TelegramEngine, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, goToVenue: @escaping (TelegramMediaMap) -> Void) -> ListViewItem { let venue = self.location let queryId = self.queryId let resultId = self.resultId @@ -71,9 +75,11 @@ private struct LocationSearchEntry: Identifiable, Comparable { header = ChatListSearchItemHeader(type: .mapAddress, theme: presentationData.theme, strings: presentationData.strings) subtitle = presentationData.strings.Map_DistanceAway(stringForDistance(strings: presentationData.strings, distance: self.distance)).string } - return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), engine: engine, venue: self.location, title: self.title, subtitle: subtitle, style: .plain, action: { + return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), engine: engine, venue: self.location, title: self.title, subtitle: subtitle, icon: .goTo, style: .plain, action: { sendVenue(venue, queryId, resultId) - }, header: header) + }, infoAction: self.story && venue.venue == nil ? { + goToVenue(venue) + } : nil, header: header) } } @@ -86,12 +92,12 @@ struct LocationSearchContainerTransition { let isEmpty: Bool } -private func locationSearchContainerPreparedTransition(from fromEntries: [LocationSearchEntry], to toEntries: [LocationSearchEntry], query: String, isSearching: Bool, isEmpty: Bool, engine: TelegramEngine, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void) -> LocationSearchContainerTransition { +private func locationSearchContainerPreparedTransition(from fromEntries: [LocationSearchEntry], to toEntries: [LocationSearchEntry], query: String, isSearching: Bool, isEmpty: Bool, engine: TelegramEngine, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, goToVenue: @escaping (TelegramMediaMap) -> Void) -> LocationSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, presentationData: presentationData, sendVenue: sendVenue), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, presentationData: presentationData, sendVenue: sendVenue), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, presentationData: presentationData, sendVenue: sendVenue, goToVenue: goToVenue), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, presentationData: presentationData, sendVenue: sendVenue, goToVenue: goToVenue), directionHint: nil) } return LocationSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, query: query, isSearching: isSearching, isEmpty: isEmpty) } @@ -99,6 +105,7 @@ private func locationSearchContainerPreparedTransition(from fromEntries: [Locati final class LocationSearchContainerNode: ASDisplayNode { private let context: AccountContext private let interaction: LocationPickerInteraction + private let story: Bool private let dimNode: ASDisplayNode public let listNode: ListView @@ -122,6 +129,7 @@ final class LocationSearchContainerNode: ASDisplayNode { public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, coordinate: CLLocationCoordinate2D, interaction: LocationPickerInteraction, story: Bool) { self.context = context self.interaction = interaction + self.story = story let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData @@ -162,6 +170,8 @@ final class LocationSearchContainerNode: ASDisplayNode { let currentLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) let themeAndStringsPromise = self.themeAndStringsPromise + let locale = localeWithStrings(presentationData.strings) + let isSearching = self._isSearching let searchItems = self.searchQuery.get() |> mapToSignal { query -> Signal in @@ -178,7 +188,6 @@ final class LocationSearchContainerNode: ASDisplayNode { |> afterCompleted { isSearching.set(false) } - let locale = localeWithStrings(presentationData.strings) let foundPlacemarks = geocodeLocation(address: query, locale: locale) return combineLatest(foundVenues, foundPlacemarks, themeAndStringsPromise.get()) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) @@ -194,9 +203,13 @@ final class LocationSearchContainerNode: ASDisplayNode { guard let placemarkLocation = placemark.location else { continue } - let location = TelegramMediaMap(latitude: placemarkLocation.coordinate.latitude, longitude: placemarkLocation.coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + var address: MapGeoAddress? + if let countryCode = placemark.isoCountryCode, placemark.thoroughfare == nil { + address = MapGeoAddress(country: countryCode, state: placemark.administrativeArea, city: placemark.locality, street: nil) + } + let location = TelegramMediaMap(latitude: placemarkLocation.coordinate.latitude, longitude: placemarkLocation.coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, address: address, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) - entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: location, queryId: nil, resultId: nil, title: placemark.name ?? "Name", distance: placemarkLocation.distance(from: currentLocation))) + entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: location, queryId: nil, resultId: nil, title: placemark.name ?? "Name", distance: placemarkLocation.distance(from: currentLocation), story: story)) index += 1 } @@ -207,7 +220,7 @@ final class LocationSearchContainerNode: ASDisplayNode { switch result.message { case let .mapLocation(mapMedia, _): if let _ = mapMedia.venue { - entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: mapMedia, queryId: contextResult.queryId, resultId: result.id, title: nil, distance: 0.0)) + entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: mapMedia, queryId: contextResult.queryId, resultId: result.id, title: nil, distance: 0.0, story: story)) index += 1 } default: @@ -235,10 +248,21 @@ final class LocationSearchContainerNode: ASDisplayNode { self?.listNode.clearHighlightAnimated(true) if let _ = venue.venue { self?.interaction.sendVenue(venue, queryId, resultId) + } else if story, let address = venue.address { + let name: String + if let city = address.city { + name = city + } else { + name = displayCountryName(address.country, locale: locale) + } + self?.interaction.sendLocation(venue.coordinate, name, address) } else { - self?.interaction.goToCoordinate(venue.coordinate) + self?.interaction.goToCoordinate(venue.coordinate, false) self?.interaction.dismissSearch() } + }, goToVenue: { venue in + self?.interaction.goToCoordinate(venue.coordinate, true) + self?.interaction.dismissSearch() }) strongSelf.enqueueTransition(transition) } diff --git a/submodules/LocationUI/Sources/LocationViewController.swift b/submodules/LocationUI/Sources/LocationViewController.swift index 35fb7bb4d46..173b93567c2 100644 --- a/submodules/LocationUI/Sources/LocationViewController.swift +++ b/submodules/LocationUI/Sources/LocationViewController.swift @@ -80,7 +80,6 @@ public final class LocationViewController: ViewController { private let isStoryLocation: Bool private let locationManager = LocationManager() - private var permissionDisposable: Disposable? private var interaction: LocationViewInteraction? diff --git a/submodules/MediaPickerUI/BUILD b/submodules/MediaPickerUI/BUILD index de41753b897..1c9cca0604d 100644 --- a/submodules/MediaPickerUI/BUILD +++ b/submodules/MediaPickerUI/BUILD @@ -50,6 +50,7 @@ swift_library( "//submodules/ChatSendMessageActionUI", "//submodules/ComponentFlow", "//submodules/Components/ComponentDisplayAdapters", + "//submodules/AnimatedCountLabelNode", ], visibility = [ "//visibility:public", diff --git a/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift b/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift index 93289e50596..4c5a43ebd72 100644 --- a/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift @@ -160,6 +160,7 @@ public final class MediaGroupsScreen: ViewController, AttachmentContainable { public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } + public var isMinimized: Bool = false public var mediaPickerContext: AttachmentMediaPickerContext? { return nil diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index c5cab76799d..4c2873fbef9 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -585,9 +585,7 @@ final class MediaPickerGridItemNode: GridItemNode { var typeIcon: UIImage? var duration: String? - if asset.isFavorite { - typeIcon = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Favorite"), color: .white) - } else if asset.mediaType == .video { + if asset.mediaType == .video { if asset.mediaSubtypes.contains(.videoHighFrameRate) { typeIcon = UIImage(bundleImageName: "Media Editor/MediaSlomo") } else if asset.mediaSubtypes.contains(.videoTimelapse) { @@ -597,6 +595,9 @@ final class MediaPickerGridItemNode: GridItemNode { } duration = stringForDuration(Int32(asset.duration)) } + if asset.isFavorite { + typeIcon = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Favorite"), color: .white) + } if typeIcon != nil { if self.leftShadowNode.supernode == nil { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 57c0191e955..4a63f4bb802 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -30,6 +30,7 @@ import CameraScreen import MediaEditor import ImageObjectSeparation import ChatSendMessageActionUI +import AnimatedCountLabelNode final class MediaPickerInteraction { let downloadManager: AssetDownloadManager @@ -192,12 +193,14 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { private let bannedSendPhotos: (Int32, Bool)? private let bannedSendVideos: (Int32, Bool)? private let canBoostToUnrestrict: Bool - private let paidMediaAllowed: Bool + fileprivate let paidMediaAllowed: Bool private let subject: Subject private let saveEditedPhotos: Bool private let titleView: MediaPickerTitleView + private let cancelButtonNode: WebAppCancelButtonNode private let moreButtonNode: MoreButtonNode + private let selectedButtonNode: SelectedButtonNode public weak var webSearchController: WebSearchController? @@ -226,10 +229,16 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } + public var isMinimized: Bool = false public var getCurrentSendMessageContextMediaPreview: (() -> ChatSendMessageContextScreenMediaPreview?)? = nil private let selectedCollection = Promise(nil) + private var selectedCollectionValue: PHAssetCollection? { + didSet { + self.selectedCollection.set(.single(self.selectedCollectionValue)) + } + } var dismissAll: () -> Void = { } @@ -938,8 +947,6 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } - - var previousEntries = self.currentEntries if self.resetOnUpdate { @@ -995,7 +1002,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.backgroundNode.updateColor(color: self.presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate) } - private var currentDisplayMode: DisplayMode = .all + private(set) var currentDisplayMode: DisplayMode = .all { + didSet { + self.displayModeUpdated(self.currentDisplayMode) + } + } + var displayModeUpdated: (DisplayMode) -> Void = { _ in } + func updateDisplayMode(_ displayMode: DisplayMode, animated: Bool = true) { let updated = self.currentDisplayMode != displayMode self.currentDisplayMode = displayMode @@ -1790,8 +1803,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.titleView.title = collection.localizedTitle ?? presentationData.strings.Attachment_Gallery } else { switch mode { - case .default, .createSticker: + case .default: + self.titleView.title = presentationData.strings.MediaPicker_Recents + self.titleView.isEnabled = true + case .createSticker: self.titleView.title = presentationData.strings.MediaPicker_Recents + self.titleView.subtitle = presentationData.strings.MediaPicker_CreateSticker self.titleView.isEnabled = true case .story: self.titleView.title = presentationData.strings.MediaPicker_Recents @@ -1806,9 +1823,15 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.titleView.title = presentationData.strings.Attachment_Gallery } + self.cancelButtonNode = WebAppCancelButtonNode(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.moreButtonNode = MoreButtonNode(theme: self.presentationData.theme) self.moreButtonNode.iconNode.enqueueState(.more, animated: false) + self.selectedButtonNode = SelectedButtonNode(theme: self.presentationData.theme) + self.selectedButtonNode.alpha = 0.0 + self.selectedButtonNode.transform = CATransform3DMakeScale(0.01, 0.01, 1.0) + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData)) self.statusBar.statusBarStyle = .Ignore @@ -1909,7 +1932,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { if case let .assets(collection, _) = self.subject, collection != nil { self.navigationItem.leftBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.backPressed)) } else { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: self.cancelButtonNode) + self.navigationItem.leftBarButtonItem?.action = #selector(self.cancelPressed) + self.navigationItem.leftBarButtonItem?.target = self + +// self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) } if self.bannedSendPhotos != nil && self.bannedSendVideos != nil { @@ -1926,6 +1953,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } + self.selectedButtonNode.addTarget(self, action: #selector(self.selectedPressed), forControlEvents: .touchUpInside) + self.scrollToTop = { [weak self] in if let strongSelf = self { if let webSearchController = strongSelf.webSearchController { @@ -2053,6 +2082,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self._ready.set(self.controllerNode.ready.get()) + self.controllerNode.displayModeUpdated = { [weak self] _ in + guard let self else { + return + } + let count = Int32(self.interaction?.selectionState?.count() ?? 0) + self.updateSelectionState(count: count) + } if case .media = self.subject { self.controllerNode.updateDisplayMode(.selected, animated: false) } @@ -2089,10 +2125,10 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } self.controllerNode.resetOnUpdate = true if collection.assetCollectionSubtype == .smartAlbumUserLibrary { - self.selectedCollection.set(.single(nil)) + self.selectedCollectionValue = nil self.titleView.title = self.presentationData.strings.MediaPicker_Recents } else { - self.selectedCollection.set(.single(collection)) + self.selectedCollectionValue = collection self.titleView.title = collection.localizedTitle ?? "" } self.scrollToTop?() @@ -2261,10 +2297,14 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { }) } - private var selectionCount: Int32 = 0 + fileprivate var selectionCount: Int32 = 0 fileprivate func updateSelectionState(count: Int32) { self.selectionCount = count + guard let layout = self.validLayout else { + return + } + let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) var moreIsVisible = false if case let .assets(_, mode) = self.subject, [.story, .createSticker].contains(mode) { moreIsVisible = true @@ -2274,29 +2314,43 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { moreIsVisible = true // self.moreButtonNode.iconNode.enqueueState(.more, animated: false) } else { - if count > 0 { - self.titleView.segments = [self.presentationData.strings.Attachment_AllMedia, self.presentationData.strings.Attachment_SelectedMedia(count)] - self.titleView.segmentsHidden = false - moreIsVisible = true -// self.moreButtonNode.iconNode.enqueueState(.more, animated: true) + let title: String + let isEnabled: Bool + if self.controllerNode.currentDisplayMode == .selected { + title = self.presentationData.strings.Attachment_SelectedMedia(count) + isEnabled = false } else { - self.titleView.segmentsHidden = true - moreIsVisible = false -// self.moreButtonNode.iconNode.enqueueState(.search, animated: true) - - if self.titleView.index != 0 { - Queue.mainQueue().after(0.3) { - self.titleView.index = 0 - } - } + title = self.selectedCollectionValue?.localizedTitle ?? self.presentationData.strings.MediaPicker_Recents + isEnabled = true + } + self.titleView.updateTitle(title: title, isEnabled: isEnabled, animated: true) + self.cancelButtonNode.setState(isEnabled ? .cancel : .back, animated: true) + + let selectedSize = self.selectedButtonNode.update(count: count) + + var safeInset: CGFloat = 0.0 + if layout.safeInsets.right > 0.0 { + safeInset += layout.safeInsets.right + 16.0 + } + let navigationHeight = navigationLayout(layout: layout).navigationFrame.height + self.selectedButtonNode.frame = CGRect(origin: CGPoint(x: self.view.bounds.width - 54.0 - selectedSize.width - safeInset, y: floorToScreenPixels((navigationHeight - selectedSize.height) / 2.0) + 1.0), size: selectedSize) + + let isSelectionButtonVisible = count > 0 && self.controllerNode.currentDisplayMode == .all + transition.updateAlpha(node: self.selectedButtonNode, alpha: isSelectionButtonVisible ? 1.0 : 0.0) + transition.updateTransformScale(node: self.selectedButtonNode, scale: isSelectionButtonVisible ? 1.0 : 0.01) + + if self.selectedButtonNode.supernode == nil { + self.navigationBar?.addSubnode(self.selectedButtonNode) } + + self.titleView.segmentsHidden = true + moreIsVisible = count > 0 } // MARK: Nicegram RoundedVideos maybeShowRoundedVideoTooltip() // - let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) transition.updateAlpha(node: self.moreButtonNode.iconNode, alpha: moreIsVisible ? 1.0 : 0.0) transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1) } @@ -2304,7 +2358,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { private func updateThemeAndStrings() { self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) self.titleView.theme = self.presentationData.theme + self.cancelButtonNode.theme = self.presentationData.theme self.moreButtonNode.theme = self.presentationData.theme + self.selectedButtonNode.theme = self.presentationData.theme self.controllerNode.updatePresentationData(self.presentationData) } @@ -2362,13 +2418,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { return true } } - - @objc private func cancelPressed() { - self.dismissAllTooltips() - self.dismiss() - } - public override func dismiss(completion: (() -> Void)? = nil) { self.controllerNode.cancelAssetDownloads() @@ -2466,6 +2516,19 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.groupsController = groupsController } + @objc private func cancelPressed() { + self.dismissAllTooltips() + if case .back = self.cancelButtonNode.state { + self.controllerNode.updateDisplayMode(.all) + } else { + self.dismiss() + } + } + + @objc private func selectedPressed() { + self.controllerNode.updateDisplayMode(.selected, animated: true) + } + @objc private func searchOrMorePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { guard self.moreButtonNode.iconNode.alpha > 0.0 else { return @@ -2632,7 +2695,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { titleLayout = .secondLineWithValue(strings.Attachment_Paid_EditPrice_Stars(Int32(price))) } else { title = strings.Attachment_Paid_Create - titleLayout = .singleLine + titleLayout = .twoLinesMax } items.append(.action(ContextMenuActionItem(text: title, textLayout: titleLayout, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor) @@ -2687,9 +2750,16 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - + self.validLayout = layout self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + + var safeInset: CGFloat = 0.0 + if layout.safeInsets.right > 0.0 { + safeInset += layout.safeInsets.right + 16.0 + } + let navigationHeight = navigationLayout(layout: layout).navigationFrame.height + self.selectedButtonNode.frame = CGRect(origin: CGPoint(x: self.view.bounds.width - 54.0 - self.selectedButtonNode.frame.width - safeInset, y: floorToScreenPixels((navigationHeight - self.selectedButtonNode.frame.height) / 2.0) + 1.0), size: self.selectedButtonNode.frame.size) } public var mediaPickerContext: AttachmentMediaPickerContext? { @@ -2744,6 +2814,45 @@ final class MediaPickerContext: AttachmentMediaPickerContext { return isForcedCaption } + var canMakePaidContent: Bool { + guard let controller = self.controller else { + return false + } + var isPaidAvailable = false + if controller.paidMediaAllowed && controller.selectionCount <= 10 { + isPaidAvailable = true + } + return isPaidAvailable + } + + var price: Int64? { + guard let controller = self.controller else { + return nil + } + var price: Int64? + if let selectionContext = controller.interaction?.selectionState, let editingContext = controller.interaction?.editingState { + for case let item as TGMediaEditableItem in selectionContext.selectedItems() { + if price == nil, let itemPrice = editingContext.price(for: item) as? Int64 { + price = itemPrice + break + } + } + } + return price + } + + func setPrice(_ price: Int64) { + guard let controller = self.controller else { + return + } + if let selectionContext = controller.interaction?.selectionState, let editingContext = controller.interaction?.editingState { + selectionContext.selectionLimit = 10 + for case let item as TGMediaEditableItem in selectionContext.selectedItems() { + editingContext.setPrice(NSNumber(value: price), for: item) + } + } + } + var captionIsAboveMedia: Signal { return Signal { [weak self] subscriber in guard let interaction = self?.controller?.interaction else { @@ -2974,12 +3083,16 @@ public func mediaPickerController( public func storyMediaPickerController( context: AccountContext, + isDark: Bool, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void ) -> ViewController { - let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) + var presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + if isDark { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: { return nil @@ -3170,3 +3283,66 @@ public func stickerMediaPickerController( controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) return controller } + +private class SelectedButtonNode: HighlightableButtonNode { + private let background = ASImageNode() + private let icon = ASImageNode() + private let label = ImmediateAnimatedCountLabelNode() + + var theme: PresentationTheme { + didSet { + self.icon.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/SelectedIcon"), color: self.theme.list.itemCheckColors.foregroundColor) + self.background.image = generateStretchableFilledCircleImage(radius: 21.0 / 2.0, color: self.theme.list.itemCheckColors.fillColor) + let _ = self.update(count: self.count) + } + } + + private var count: Int32 = 0 + + init(theme: PresentationTheme) { + self.theme = theme + + super.init() + + self.background.displaysAsynchronously = false + self.icon.displaysAsynchronously = false + self.label.displaysAsynchronously = false + + self.icon.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/SelectedIcon"), color: self.theme.list.itemCheckColors.foregroundColor) + self.background.image = generateStretchableFilledCircleImage(radius: 21.0 / 2.0, color: self.theme.list.itemCheckColors.fillColor) + + self.addSubnode(self.background) + self.addSubnode(self.icon) + self.addSubnode(self.label) + } + + func update(count: Int32) -> CGSize { + self.count = count + + let diameter: CGFloat = 21.0 + let font = Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]) + + let stringValue = "\(max(1, count))" + var segments: [AnimatedCountLabelNode.Segment] = [] + for char in stringValue { + if let intValue = Int(String(char)) { + segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: self.theme.list.itemCheckColors.foregroundColor))) + } + } + self.label.segments = segments + + let textSize = self.label.updateLayout(size: CGSize(width: 100.0, height: diameter), animated: true) + let size = CGSize(width: textSize.width + 28.0, height: diameter) + + if let _ = self.icon.image { + let iconSize = CGSize(width: 14.0, height: 11.0) + let iconFrame = CGRect(origin: CGPoint(x: 5.0, y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) + self.icon.frame = iconFrame + } + + self.label.frame = CGRect(origin: CGPoint(x: 21.0, y: floor((size.height - textSize.height) / 2.0) - UIScreenPixel), size: textSize) + self.background.frame = CGRect(origin: .zero, size: size) + + return size + } +} diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index 78caaba0fe9..8b5b6960ac7 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -1046,7 +1046,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS if !self.isExternalPreview { let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)) var peers = SimpleDictionary() - peers[peerId] = TelegramUser(id: peerId, 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) + peers[peerId] = TelegramUser(id: peerId, 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) let previewText = groupLayouts.count > 1 ? presentationData.strings.Attachment_MessagesPreview : presentationData.strings.Attachment_MessagePreview diff --git a/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift b/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift index 2cae7588f23..a9d360915bd 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift @@ -10,12 +10,14 @@ final class MediaPickerTitleView: UIView { let contextSourceNode: ContextReferenceContentNode private let buttonNode: HighlightTrackingButtonNode private let titleNode: ImmediateTextNode + private let subtitleNode: ImmediateTextNode private let arrowNode: ASImageNode private let segmentedControlNode: SegmentedControlNode public var theme: PresentationTheme { didSet { self.titleNode.attributedText = NSAttributedString(string: self.title, font: NavigationBar.titleFont, textColor: theme.rootController.navigationBar.primaryTextColor) + self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle, font: Font.regular(12.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme)) } } @@ -23,7 +25,16 @@ final class MediaPickerTitleView: UIView { public var title: String = "" { didSet { if self.title != oldValue { - self.titleNode.attributedText = NSAttributedString(string: self.title, font: NavigationBar.titleFont, textColor: theme.rootController.navigationBar.primaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: self.title, font: NavigationBar.titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) + self.setNeedsLayout() + } + } + } + + public var subtitle: String = "" { + didSet { + if self.subtitle != oldValue { + self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle, font: Font.regular(12.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) self.setNeedsLayout() } } @@ -36,6 +47,47 @@ final class MediaPickerTitleView: UIView { } } + public func updateTitle(title: String, subtitle: String = "", isEnabled: Bool, animated: Bool) { + if animated { + if self.title != title { + if let snapshotView = self.titleNode.view.snapshotContentTree() { + snapshotView.frame = self.titleNode.frame + self.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + self.titleNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + if self.subtitle != subtitle { + if let snapshotView = self.subtitleNode.view.snapshotContentTree() { + snapshotView.frame = self.subtitleNode.frame + self.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + self.subtitleNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + if self.isEnabled != isEnabled { + if let snapshotView = self.arrowNode.view.snapshotContentTree() { + snapshotView.frame = self.arrowNode.frame + self.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + self.arrowNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + } + self.title = title + self.subtitle = subtitle + self.isEnabled = isEnabled + } + public var isHighlighted: Bool = false { didSet { self.alpha = self.isHighlighted ? 0.5 : 1.0 @@ -45,8 +97,9 @@ final class MediaPickerTitleView: UIView { public var segmentsHidden = true { didSet { if self.segmentsHidden != oldValue { - let transition = ContainedViewLayoutTransition.animated(duration: 0.21, curve: .easeInOut) + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) transition.updateAlpha(node: self.titleNode, alpha: self.segmentsHidden ? 1.0 : 0.0) + transition.updateAlpha(node: self.subtitleNode, alpha: self.segmentsHidden ? 1.0 : 0.0) transition.updateAlpha(node: self.arrowNode, alpha: self.segmentsHidden ? 1.0 : 0.0) transition.updateAlpha(node: self.segmentedControlNode, alpha: self.segmentsHidden ? 0.0 : 1.0) self.segmentedControlNode.isUserInteractionEnabled = !self.segmentsHidden @@ -86,6 +139,9 @@ final class MediaPickerTitleView: UIView { self.titleNode = ImmediateTextNode() self.titleNode.displaysAsynchronously = false + self.subtitleNode = ImmediateTextNode() + self.subtitleNode.displaysAsynchronously = false + self.arrowNode = ASImageNode() self.arrowNode.displaysAsynchronously = false self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/DownArrow"), color: theme.rootController.navigationBar.secondaryTextColor) @@ -116,6 +172,7 @@ final class MediaPickerTitleView: UIView { self.addSubnode(self.contextSourceNode) self.addSubnode(self.titleNode) + self.addSubnode(self.subtitleNode) self.addSubnode(self.arrowNode) self.addSubnode(self.buttonNode) self.addSubnode(self.segmentedControlNode) @@ -137,10 +194,18 @@ final class MediaPickerTitleView: UIView { self.segmentedControlNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - controlSize.width) / 2.0), y: floorToScreenPixels((size.height - controlSize.height) / 2.0)), size: controlSize) let titleSize = self.titleNode.updateLayout(CGSize(width: 210.0, height: 44.0)) - self.titleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: 210.0, height: 44.0)) + + var totalHeight: CGFloat = titleSize.height + if subtitleSize.height > 0.0 { + totalHeight += subtitleSize.height + } + + self.titleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - totalHeight) / 2.0)), size: titleSize) + self.subtitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - subtitleSize.width) / 2.0), y: floorToScreenPixels((size.height - totalHeight) / 2.0) + subtitleSize.height + 7.0), size: subtitleSize) if let arrowSize = self.arrowNode.image?.size { - self.arrowNode.frame = CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 5.0, y: floorToScreenPixels((size.height - arrowSize.height) / 2.0) + 1.0 - UIScreenPixel), size: arrowSize) + self.arrowNode.frame = CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 5.0, y: floorToScreenPixels((size.height - totalHeight) / 2.0) + titleSize.height / 2.0 - arrowSize.height / 2.0 + 1.0 - UIScreenPixel), size: arrowSize) } self.buttonNode.frame = CGRect(origin: .zero, size: size) } diff --git a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTBindKeyMessageService.h b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTBindKeyMessageService.h index 9e5771a203d..8e2fd4e9587 100644 --- a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTBindKeyMessageService.h +++ b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTBindKeyMessageService.h @@ -6,4 +6,5 @@ - (instancetype)initWithPersistentKey:(MTDatacenterAuthKey *)persistentKey ephemeralKey:(MTDatacenterAuthKey *)ephemeralKey completion:(void (^)(bool))completion; +-(void)complete; @end diff --git a/submodules/MtProtoKit/Sources/MTApiEnvironment.m b/submodules/MtProtoKit/Sources/MTApiEnvironment.m index e9aeecf4a72..1b9491b9396 100644 --- a/submodules/MtProtoKit/Sources/MTApiEnvironment.m +++ b/submodules/MtProtoKit/Sources/MTApiEnvironment.m @@ -10,75 +10,6 @@ #import -typedef enum { - UIDeviceUnknown, - - UIDeviceSimulator, - - UIDevice1GiPhone, - UIDevice3GiPhone, - UIDevice3GSiPhone, - UIDevice4iPhone, - UIDevice4SiPhone, - UIDevice5iPhone, - UIDevice5SiPhone, - UIDevice6iPhone, - UIDevice6PlusiPhone, - UIDevice6SiPhone, - UIDevice6SPlusiPhone, - UIDevice7iPhone, - UIDevice7PlusiPhone, - UIDevice8iPhone, - UIDevice8PlusiPhone, - UIDeviceXiPhone, - UIDeviceSEPhone, - UIDeviceSE2Phone, - UIDeviceXSiPhone, - UIDeviceXSMaxiPhone, - UIDeviceXRiPhone, - UIDevice11iPhone, - UIDevice11ProiPhone, - UIDevice11ProMaxiPhone, - UIDevice12MiniiPhone, - UIDevice12iPhone, - UIDevice12ProiPhone, - UIDevice12ProMaxiPhone, - - UIDevice1GiPod, - UIDevice2GiPod, - UIDevice3GiPod, - UIDevice4GiPod, - UIDevice5GiPod, - UIDevice6GiPod, - UIDevice7GiPod, - - UIDevice1GiPad, - UIDevice2GiPad, - UIDevice3GiPad, - UIDevice4GiPad, - UIDevice5GiPad, - UIDevice6GiPad, - - UIDeviceiPadPro12_93g, - UIDeviceiPadPro11, - UIDeviceiPadPro6g, - UIDeviceiPadPro10_5, - UIDeviceiPadPro12_9, - - UIDeviceAppleTV2, - UIDeviceAppleTV3, - UIDeviceAppleTV4, - - UIDeviceUnknowniPhone, - UIDeviceUnknowniPod, - UIDeviceUnknowniPad, - UIDeviceUnknownAppleTV, - UIDeviceIFPGA, - - UIDeviceOSX - -} UIDevicePlatform; - static NSData * _Nullable parseHexString(NSString * _Nonnull hex) { if ([hex length] % 2 != 0) { return nil; @@ -758,7 +689,23 @@ - (NSString *)platformString if ([platform isEqualToString:@"iPad14,5"] || [platform isEqualToString:@"iPad14,6"]) return @"iPad Pro 12.9 inch (6th gen)"; - + + if ([platform isEqualToString:@"iPad14,8"] || + [platform isEqualToString:@"iPad14,9"]) + return @"iPad Air (6th gen)"; + + if ([platform isEqualToString:@"iPad14,10"] || + [platform isEqualToString:@"iPad14,11"]) + return @"iPad Air (7th gen)"; + + if ([platform isEqualToString:@"iPad16,3"] || + [platform isEqualToString:@"iPad16,4"]) + return @"iPad Pro 11 inch (5th gen)"; + + if ([platform isEqualToString:@"iPad16,5"] || + [platform isEqualToString:@"iPad16,6"]) + return @"iPad Pro 12.9 inch (7th gen)"; + if ([platform hasPrefix:@"iPhone"]) return @"Unknown iPhone"; if ([platform hasPrefix:@"iPod"]) diff --git a/submodules/MtProtoKit/Sources/MTBindKeyMessageService.m b/submodules/MtProtoKit/Sources/MTBindKeyMessageService.m index 137b2ce4087..81444d51ab3 100644 --- a/submodules/MtProtoKit/Sources/MTBindKeyMessageService.m +++ b/submodules/MtProtoKit/Sources/MTBindKeyMessageService.m @@ -168,4 +168,8 @@ - (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message } } +-(void)complete { + _completion(true); +} + @end diff --git a/submodules/MtProtoKit/Sources/MTContext.m b/submodules/MtProtoKit/Sources/MTContext.m index 7e506f5edcd..a86308e06ae 100644 --- a/submodules/MtProtoKit/Sources/MTContext.m +++ b/submodules/MtProtoKit/Sources/MTContext.m @@ -250,7 +250,7 @@ - (instancetype)initWithSerialization:(id)serialization encrypt _tempKeyExpiration = 24 * 60 * 60; #if DEBUG - //_tempKeyExpiration = 30; + //_tempKeyExpiration = 30; #endif _datacenterSeedAddressSetById = [[NSMutableDictionary alloc] init]; diff --git a/submodules/MtProtoKit/Sources/MTProto.m b/submodules/MtProtoKit/Sources/MTProto.m index 965ee029d09..2e31704d7e4 100644 --- a/submodules/MtProtoKit/Sources/MTProto.m +++ b/submodules/MtProtoKit/Sources/MTProto.m @@ -55,7 +55,7 @@ #import #import - +#import typedef enum { MTProtoStateAwaitingDatacenterScheme = 1, MTProtoStateAwaitingDatacenterAuthorization = 2, @@ -885,7 +885,7 @@ - (NSString *)incomingMessageDescription:(MTIncomingMessage *)message - (MTDatacenterAuthKey *)getAuthKeyForCurrentScheme:(MTTransportScheme *)scheme createIfNeeded:(bool)createIfNeeded authInfoSelector:(MTDatacenterAuthInfoSelector *)authInfoSelector { if (_useExplicitAuthKey) { - MTDatacenterAuthInfoSelector selector = MTDatacenterAuthInfoSelectorEphemeralMain; + MTDatacenterAuthInfoSelector selector = scheme.media ? MTDatacenterAuthInfoSelectorEphemeralMedia : MTDatacenterAuthInfoSelectorEphemeralMain; if (authInfoSelector != nil) { *authInfoSelector = selector; } @@ -2151,6 +2151,15 @@ - (void)handleMissingKey:(MTTransportScheme *)scheme { } if (_useExplicitAuthKey != nil) { + if (scheme.media) { + for (NSInteger i = (NSInteger)_messageServices.count - 1; i >= 0; i--) + { + MTBindKeyMessageService* messageService = (MTBindKeyMessageService *)_messageServices[(NSUInteger)i]; + if ([messageService respondsToSelector:@selector(complete)]) { + [messageService complete]; + } + } + } } else if (_cdn) { _validAuthInfo = nil; diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift index e359d7895ba..cdc9c00032d 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift @@ -286,7 +286,7 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope })) } - options.append(OpenInOption(identifier: "2gis", application: .other(title: "2GIS", identifier: 481627348, scheme: "dgis", store: nil), action: { + options.append(OpenInOption(identifier: "2gis", application: .other(title: "2GIS", identifier: 481627348, scheme: "dgis", store: "ru"), action: { let coordinates = "\(lon),\(lat)" if let _ = directions { return .openUrl(url: "dgis://2gis.ru/routeSearch/to/\(coordinates)/go") diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index 539b78af181..9ec039326db 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift @@ -1104,7 +1104,7 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega self.hideButtonNode.isHidden = confirmation case .email: self.inputNode.textField.keyboardType = .emailAddress - self.inputNode.textField.returnKeyType = .done + self.inputNode.textField.returnKeyType = .next self.hideButtonNode.isHidden = true if #available(iOS 12.0, *) { @@ -1134,7 +1134,7 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega } case .hint: self.inputNode.textField.keyboardType = .asciiCapable - self.inputNode.textField.returnKeyType = .done + self.inputNode.textField.returnKeyType = .next self.hideButtonNode.isHidden = true self.inputNode.textField.autocorrectionType = .no diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index 6a74a5da183..37b5bc76e64 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -187,7 +187,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { let subject: ShareControllerSubject var actionCompletionText: String? if let video = entry.videoRepresentations.last, let peerReference = PeerReference(peer._asPeer()) { - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil)])) + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) subject = .media(videoFileReference.abstract) actionCompletionText = strongSelf.presentationData.strings.Gallery_VideoSaved } else { @@ -279,7 +279,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { if let video = entry.videoRepresentations.last, let peerReference = PeerReference(self.peer._asPeer()) { if video != previousVideoRepresentations?.last { let mediaManager = self.context.sharedContext.mediaManager - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: entry.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil)])) + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: entry.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) let videoContent = NativeVideoContent(id: .profileVideo(id, category), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: true, useLargeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay) videoNode.isUserInteractionEnabled = false diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index 9df6da5510e..36d3ff73aae 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -519,7 +519,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { self.isReady.set(.single(true)) } } else if let video = videoRepresentations.last, let peerReference = PeerReference(self.peer._asPeer()) { - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil)])) + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) let videoContent = NativeVideoContent(id: .profileVideo(id, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: fullSizeOnly, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { @@ -1135,7 +1135,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { let subject: ShareControllerSubject var actionCompletionText: String? if let video = videoRepresentations.last, let peer = self.peer, let peerReference = PeerReference(peer._asPeer()) { - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil)])) + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) subject = .media(videoFileReference.abstract) actionCompletionText = presentationData.strings.Gallery_VideoSaved } else { diff --git a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift index 05d9664877f..db90962e575 100644 --- a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +++ b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift @@ -671,7 +671,7 @@ private func deviceContactInfoEntries(context: ShareControllerAccountContext, pr firstName = presentationData.strings.Message_Contact } - entries.append(.info(entries.count, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer: peer ?? EnginePeer.user(TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: firstName, lastName: isOrganization ? nil : personName.1, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)), state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), job: isOrganization ? nil : jobSummary, isPlain: !isShare, hiddenAvatar: hiddenAvatar)) + entries.append(.info(entries.count, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer: peer ?? EnginePeer.user(TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: firstName, lastName: isOrganization ? nil : personName.1, 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)), state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), job: isOrganization ? nil : jobSummary, isPlain: !isShare, hiddenAvatar: hiddenAvatar)) if !selecting { if let _ = peer { diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 84a72e48975..c3b5f6000d0 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -56,6 +56,10 @@ public func representationFetchRangeForDisplayAtSize(representation: TelegramMed } public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, useMiniThumbnailIfAvailable: Bool = false, forceThumbnail: Bool = false, automaticFetch: Bool = true) -> Signal, NoError> { + return chatMessagePhotoDatas(mediaBox: postbox.mediaBox, userLocation: userLocation, customUserContentType: customUserContentType, photoReference: photoReference, fullRepresentationSize: fullRepresentationSize, autoFetchFullSize: autoFetchFullSize, tryAdditionalRepresentations: tryAdditionalRepresentations, synchronousLoad: synchronousLoad, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable, forceThumbnail: forceThumbnail, automaticFetch: automaticFetch) +} + +func chatMessagePhotoDatas(mediaBox: MediaBox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, useMiniThumbnailIfAvailable: Bool = false, forceThumbnail: Bool = false, automaticFetch: Bool = true) -> Signal, NoError> { if !forceThumbnail, let progressiveRepresentation = progressiveImageRepresentation(photoReference.media.representations), progressiveRepresentation.progressiveSizes.count > 1 { enum SizeSource { case miniThumbnail(data: Data) @@ -93,7 +97,7 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU case let .miniThumbnail(data): return .single((source, data)) case let .image(size): - return postbox.mediaBox.resourceData(progressiveRepresentation.resource, size: Int64(progressiveRepresentation.progressiveSizes.last!), in: 0 ..< size, mode: .incremental, notifyAboutIncomplete: true, attemptSynchronously: synchronousLoad) + return mediaBox.resourceData(progressiveRepresentation.resource, size: Int64(progressiveRepresentation.progressiveSizes.last!), in: 0 ..< size, mode: .incremental, notifyAboutIncomplete: true, attemptSynchronously: synchronousLoad) |> map { (data, _) -> (SizeSource, Data?) in return (source, data) } @@ -131,9 +135,9 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU var fetchDisposable: Disposable? if automaticFetch { if autoFetchFullSize { - fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(largestByteSize), .default), statsCategory: .image).start() + fetchDisposable = fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(largestByteSize), .default), statsCategory: .image).start() } else if useMiniThumbnailIfAvailable { - fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(thumbnailByteSize), .default), statsCategory: .image).start() + fetchDisposable = fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(thumbnailByteSize), .default), statsCategory: .image).start() } } @@ -145,8 +149,8 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU } if !forceThumbnail || photoReference.media.immediateThumbnailData == nil, let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(PixelDimensions(width: Int32(fullRepresentationSize.width), height: Int32(fullRepresentationSize.height))), let fullRepresentation = largestImageRepresentation(photoReference.media.representations) { - let maybeFullSize = postbox.mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) - let maybeLargestSize = postbox.mediaBox.resourceData(fullRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) + let maybeFullSize = mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) + let maybeLargestSize = mediaBox.resourceData(fullRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) let signal = combineLatest(maybeFullSize, maybeLargestSize) |> take(1) @@ -163,16 +167,16 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU if let _ = decodedThumbnailData { fetchedThumbnail = .complete() } else { - fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) + fetchedThumbnail = fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) } - let fetchedFullSize = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image) + let fetchedFullSize = fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image) let anyThumbnail: [Signal<(MediaResourceData, ChatMessagePhotoQuality), NoError>] if tryAdditionalRepresentations { anyThumbnail = photoReference.media.representations.filter({ representation in return representation != largestRepresentation }).map({ representation -> Signal<(MediaResourceData, ChatMessagePhotoQuality), NoError> in - return postbox.mediaBox.resourceData(representation.resource) + return mediaBox.resourceData(representation.resource) |> take(1) |> map { data -> (MediaResourceData, ChatMessagePhotoQuality) in if representation.dimensions.width > 200 || representation.dimensions.height > 200 { @@ -193,7 +197,7 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU return EmptyDisposable } else { let fetchedDisposable = fetchedThumbnail.start() - let thumbnailDisposable = postbox.mediaBox.resourceData(smallestRepresentation.resource, attemptSynchronously: synchronousLoad).start(next: { next in + let thumbnailDisposable = mediaBox.resourceData(smallestRepresentation.resource, attemptSynchronously: synchronousLoad).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) @@ -222,7 +226,7 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU if autoFetchFullSize && !useMiniThumbnailIfAvailable { fullSizeData = Signal, NoError> { subscriber in let fetchedFullSizeDisposable = fetchedFullSize.start() - let fullSizeDisposable = postbox.mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad).start(next: { next in + let fullSizeDisposable = mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad).start(next: { next in subscriber.putNext(Tuple(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) }, error: subscriber.putError, completed: subscriber.putCompletion) @@ -232,7 +236,7 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU } } } else { - fullSizeData = postbox.mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) + fullSizeData = mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) |> map { next -> Tuple2 in return Tuple(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) } @@ -600,6 +604,13 @@ public func chatMessagePhoto(postbox: Postbox, userLocation: MediaResourceUserLo } } +public func chatMessagePhoto(mediaBox: MediaBox, userLocation: MediaResourceUserLocation, userContentType customUserContentType: MediaResourceUserContentType? = nil, photoReference: ImageMediaReference, synchronousLoad: Bool = false, highQuality: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(mediaBox: mediaBox, userLocation: userLocation, customUserContentType: customUserContentType, photoReference: photoReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad), synchronousLoad: synchronousLoad) + |> map { _, _, generate in + return generate + } +} + public enum ChatMessagePhotoQuality { case none case blurred diff --git a/submodules/PremiumUI/Resources/gift b/submodules/PremiumUI/Resources/gift deleted file mode 100644 index 87a9c2a5e8490c3eb9efd5fa2d842144210e5f77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21826 zcmV)&K#ad1iwFo=14zHYfPdh##mxEYGO1RQ%sDpCHkM;zOZ4ln_xKo#r?Ghr6& z0SjSwSOoilj&KYlp%spWFTrteJe&Y0!k6JBI2lfXQ{gM19c&M$!DVncY=kS|O87c_ z1FnK^!qxB{xDjrJhu~rO5&Re)fuF#m@KbmU9)~C3N%$E&1y93E@JskJkifej7XAVs z!p98A;4xwu`HWtS0!D8}A4XqBA)|=Vk5SC%<d{U<_i6WQ<~rXG~%&V!Xy!$#|2o zlkqO&0OKIz9OFFWOU8A^eZ~XELyUp3F+WU*MPMDVPFMn#f+;W!mX3A7x?1Gq*5zGxsx3GCyNpWL{zZ#Jt0N#C*c?V6|oiutHc;Rst)9)t!~g%479n z6|hQK6|AwW@vJGVsjOF7^H|GRZ?e|1HnGmIZnC~%{lxl(&1Li09&B%RTXr;C%UfaZRYLb?d2Wh-QxYmd(3C^`TQV$ zFh7#to}bKD@-_S{zL7thKZn1Vzns5`|1N(o|2Y2y|2+Q^|2F?e{saDRxEt<{`{6;j z7?nXaduS`)^oU1yR8)Sg)?90^s*lcC-b-Jt>(gWbOWcJmH$z)pW6-6c zGHd&yGSW;2lew_MtgY7d)Yj_y>5N15W|OfRu~uj?)tSq51wC^LEmTcZ=>yOu8OCyZ zr6kh|SBcaB0In!~H~=IgTzMPN8`lb#S>by*wj)wW{S60An>%!Ox57Q)s_wR9D}2KW z_nrpvs9QUNP9Om!f+UcPI*Hd LhK^jPZHoI^4{9Jkp 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 E0pn~p82|tP literal 0 HcmV?d00001 diff --git a/submodules/PremiumUI/Resources/star b/submodules/PremiumUI/Resources/star deleted file mode 100644 index 3b27e0220e6c6fa2b28a4e5fd665c0c47182bc7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59131 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 R2G>sNIUJ}9kwRZ1`!7e_S!@6R diff --git a/submodules/PremiumUI/Resources/star2 b/submodules/PremiumUI/Resources/star2 new file mode 100644 index 0000000000000000000000000000000000000000..2ab06065543c4b090218d7a82211cec8d9e864ab GIT binary patch literal 67438 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 z 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∋GwVj{ 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 diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index 900a4d0cd66..453ee956af1 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -32,6 +32,8 @@ extension PremiumGiftSource { return "attach" case .settings: return "settings" + case .stars: + return "" case .chatList: return "chats" case .channelBoost: @@ -241,7 +243,6 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { } names.append("**\(context.component.peers[i].compactDisplayTitle)**") } - descriptionString = strings.Premium_Gift_MultipleDescription(names, "").string } else { for i in 0 ..< min(3, context.component.peers.count) { if i == 0 { diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index c43a7ea6016..3425dff41f8 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -2724,7 +2724,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { if url.hasPrefix("https://apps.apple.com/account/subscriptions") { controller.context.sharedContext.applicationBindings.openSubscriptions() } else if url.hasPrefix("https://") || url.hasPrefix("tg://") { - controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://") && !url.contains("?start="), presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {}) + controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: false, presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: navigationController, dismissInput: {}) } else { let context = controller.context let signal: Signal? diff --git a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift index 5a19bd9b4a5..312ebb57a88 100644 --- a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift +++ b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift @@ -163,8 +163,8 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, ASScrollViewDel let otherPeerId = self.context.account.peerId var peers = SimpleDictionary() var messages = SimpleDictionary() - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) - peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) + peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) diff --git a/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift b/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift index 6c80171197f..4885747d7f7 100644 --- a/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift +++ b/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift @@ -5,6 +5,7 @@ import AccountContext import InstantPageUI import InstantPageCache import UrlHandling +import TelegramUIPreferences func faqSearchableItems(context: AccountContext, resolvedUrl: Signal, suggestAccountDeletion: Bool) -> Signal<[SettingsSearchableItem], NoError> { let strings = context.sharedContext.currentPresentationData.with { $0 }.strings @@ -45,8 +46,9 @@ func faqSearchableItems(context: AccountContext, resolvedUrl: Signal ViewControll if MFMailComposeViewController.canSendMail() { let composeController = MFMailComposeViewController() - composeController.setToRecipients(["login@stel.com"]) + composeController.setToRecipients(["recover@telegram.org"]) composeController.setSubject(presentationData.strings.Login_PhoneBannedEmailSubject(formattedNumber).string) composeController.setMessageBody(presentationData.strings.Login_PhoneBannedEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string, isHTML: false) composeController.mailComposeDelegate = controller diff --git a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift index e5f16c12013..adf77392b48 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift @@ -100,9 +100,9 @@ private enum DataAndStorageEntry: ItemListNodeEntry { case useLessVoiceData(PresentationTheme, String, Bool) case useLessVoiceDataInfo(PresentationTheme, String) case otherHeader(PresentationTheme, String) + case openLinksIn(PresentationTheme, String, String) case shareSheet(PresentationTheme, String) case saveEditedPhotos(PresentationTheme, String, Bool) - case openLinksIn(PresentationTheme, String, String) case pauseMusicOnRecording(PresentationTheme, String, Bool) case raiseToListen(PresentationTheme, String, Bool) case raiseToListenInfo(PresentationTheme, String) @@ -123,7 +123,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return DataAndStorageSection.backgroundDownload.rawValue case .useLessVoiceData, .useLessVoiceDataInfo: return DataAndStorageSection.voiceCalls.rawValue - case .otherHeader, .shareSheet, .saveEditedPhotos, .openLinksIn, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo: + case .otherHeader, .openLinksIn, .shareSheet, .saveEditedPhotos, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo: return DataAndStorageSection.other.rawValue case .connectionHeader, .connectionProxy: return DataAndStorageSection.connection.rawValue @@ -162,11 +162,11 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return 24 case .otherHeader: return 29 - case .shareSheet: + case .openLinksIn: return 30 - case .saveEditedPhotos: + case .shareSheet: return 31 - case .openLinksIn: + case .saveEditedPhotos: return 32 case .pauseMusicOnRecording: return 33 @@ -257,20 +257,20 @@ private enum DataAndStorageEntry: ItemListNodeEntry { } else { return false } - case let .shareSheet(lhsTheme, lhsText): - if case let .shareSheet(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .openLinksIn(lhsTheme, lhsText, lhsValue): + if case let .openLinksIn(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .saveEditedPhotos(lhsTheme, lhsText, lhsValue): - if case let .saveEditedPhotos(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + case let .shareSheet(lhsTheme, lhsText): + if case let .shareSheet(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .openLinksIn(lhsTheme, lhsText, lhsValue): - if case let .openLinksIn(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + case let .saveEditedPhotos(lhsTheme, lhsText, lhsValue): + if case let .saveEditedPhotos(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false @@ -386,6 +386,10 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .otherHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .openLinksIn(_, text, value): + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { + arguments.openBrowserSelection() + }) case let .shareSheet(_, text): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openIntents() @@ -394,10 +398,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleSaveEditedPhotos(value) }, tag: DataAndStorageEntryTag.saveEditedPhotos) - case let .openLinksIn(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { - arguments.openBrowserSelection() - }) case let .pauseMusicOnRecording(_, text, value): return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.togglePauseMusicOnRecording(value) @@ -618,11 +618,11 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat entries.append(.useLessVoiceDataInfo(presentationData.theme, presentationData.strings.CallSettings_UseLessDataLongDescription)) entries.append(.otherHeader(presentationData.theme, presentationData.strings.ChatSettings_Other)) + entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser)) if #available(iOSApplicationExtension 13.2, iOS 13.2, *) { entries.append(.shareSheet(presentationData.theme, presentationData.strings.ChatSettings_IntentsSettings)) } entries.append(.saveEditedPhotos(presentationData.theme, presentationData.strings.Settings_SaveEditedPhotos, data.generatedMediaStoreSettings.storeEditedPhotos)) - entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser)) entries.append(.pauseMusicOnRecording(presentationData.theme, presentationData.strings.Settings_PauseMusicOnRecording, data.mediaInputSettings.pauseMusicOnRecording)) entries.append(.raiseToListen(presentationData.theme, presentationData.strings.Settings_RaiseToListen, data.mediaInputSettings.enableRaiseToSpeak)) entries.append(.raiseToListenInfo(presentationData.theme, presentationData.strings.Settings_RaiseToListenInfo)) @@ -917,8 +917,10 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da let defaultWebBrowser: String if let option = options.first(where: { $0.identifier == webBrowserSettings.defaultWebBrowser }) { defaultWebBrowser = option.title - } else { + } else if webBrowserSettings.defaultWebBrowser == "inApp" { defaultWebBrowser = presentationData.strings.WebBrowser_InAppSafari + } else { + defaultWebBrowser = presentationData.strings.WebBrowser_Telegram } 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) diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift new file mode 100644 index 00000000000..4d62514a07f --- /dev/null +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift @@ -0,0 +1,471 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import TelegramCore +import TelegramPresentationData +import AccountContext +import UrlEscaping +import ActivityIndicator + +private final class WebBrowserDomainInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { + private var theme: PresentationTheme + private let backgroundNode: ASImageNode + fileprivate let textInputNode: EditableTextNode + private let placeholderNode: ASTextNode + + var updateHeight: (() -> Void)? + var complete: (() -> Void)? + var textChanged: ((String) -> Void)? + + private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) + private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0) + + var text: String { + get { + return self.textInputNode.attributedText?.string ?? "" + } + set { + self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor) + self.placeholderNode.isHidden = !newValue.isEmpty + } + } + + var placeholder: String = "" { + didSet { + self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + } + } + + init(theme: PresentationTheme, placeholder: String) { + self.theme = theme + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + + self.textInputNode = EditableTextNode() + self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor] + self.textInputNode.clipsToBounds = true + self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0) + self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance + self.textInputNode.keyboardType = .URL + self.textInputNode.autocapitalizationType = .none + self.textInputNode.returnKeyType = .done + self.textInputNode.autocorrectionType = .no + self.textInputNode.tintColor = theme.actionSheet.controlAccentColor + + self.placeholderNode = ASTextNode() + self.placeholderNode.isUserInteractionEnabled = false + self.placeholderNode.displaysAsynchronously = false + self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + + super.init() + + self.textInputNode.delegate = self + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textInputNode) + self.addSubnode(self.placeholderNode) + } + + func updateTheme(_ theme: PresentationTheme) { + self.theme = theme + + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance + self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: width) + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + + let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + + let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) + transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) + + transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height))) + + return panelHeight + } + + func activateInput() { + self.textInputNode.becomeFirstResponder() + } + + func deactivateInput() { + self.textInputNode.resignFirstResponder() + } + + @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + self.updateTextNodeText(animated: true) + self.textChanged?(editableTextNode.textView.text) + self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty + } + + private let domainRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.?)*([a-zA-Z]*)?(:)?(/)?$", options: []) + private let pathRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}/", options: []) + + var inProgress = false + + func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if self.inProgress { + return false + } + if text == "\n" { + self.complete?() + return false + } + + if let domainRegex = self.domainRegex, let pathRegex = self.pathRegex { + let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) + let domainMatches = domainRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) + let pathMatches = pathRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) + + if domainMatches.count > 0, pathMatches.count == 0 { + return true + } else { + return false + } + } + + return true + } + + private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + + let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height)) + + return min(61.0, max(33.0, unboundTextFieldHeight)) + } + + private func updateTextNodeText(animated: Bool) { + let backgroundInsets = self.backgroundInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) + + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + if !self.bounds.size.height.isEqual(to: panelHeight) { + self.updateHeight?() + } + } + + @objc func clearPressed() { + self.textInputNode.attributedText = nil + self.deactivateInput() + } +} + +private final class WebBrowserDomainAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + + private let titleNode: ASTextNode + private let textNode: ASTextNode + let activityIndicator: ActivityIndicator + let inputFieldNode: WebBrowserDomainInputFieldNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private let disposable = MetaDisposable() + + private var validLayout: CGSize? + + private let hapticFeedback = HapticFeedback() + + var complete: (() -> Void)? { + didSet { + self.inputFieldNode.complete = self.complete + } + } + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction]) { + self.strings = strings + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 2 + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 2 + + self.activityIndicator = ActivityIndicator(type: .custom(ptheme.rootController.navigationBar.secondaryTextColor, 20.0, 1.5, false), speed: .slow) + self.activityIndicator.isHidden = true + + self.inputFieldNode = WebBrowserDomainInputFieldNode(theme: ptheme, placeholder: strings.WebBrowser_Exceptions_Create_Placeholder) + self.inputFieldNode.text = "" + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.addSubnode(self.inputFieldNode) + self.addSubnode(self.activityIndicator) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + self.actionNodes.last?.actionEnabled = false + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.inputFieldNode.updateHeight = { [weak self] in + if let strongSelf = self { + if let _ = strongSelf.validLayout { + strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring)) + } + } + } + + self.inputFieldNode.textChanged = { [weak self] text in + if let strongSelf = self, let lastNode = strongSelf.actionNodes.last { + lastNode.actionEnabled = !text.isEmpty + } + } + + self.updateTheme(theme) + } + + deinit { + self.disposable.dispose() + } + + var link: String { + return self.inputFieldNode.text + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) + + let hadValidLayout = self.validLayout != nil + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + let spacing: CGFloat = 5.0 + + let titleSize = self.titleNode.measure(measureSize) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 4.0 + + let textSize = self.textNode.measure(measureSize) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 6.0 + spacing + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0) + + var contentWidth = max(titleSize.width, minActionsWidth) + contentWidth = max(contentWidth, 234.0) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultWidth = contentWidth + insets.left + insets.right + + let inputFieldWidth = resultWidth + let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) + let inputHeight = inputFieldHeight + let inputFrame = CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight) + transition.updateFrame(node: self.inputFieldNode, frame: inputFrame) + transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) + + let activitySize = CGSize(width: 20.0, height: 20.0) + transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: inputFrame.maxX - activitySize.width - 23.0, y: inputFrame.midY - activitySize.height / 2.0 - 3.0), size: activitySize)) + + let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + if !hadValidLayout { + self.inputFieldNode.activateInput() + } + + return resultSize + } + + func animateError() { + self.inputFieldNode.layer.addShakeAnimation() + self.hapticFeedback.error() + } +} + +public func webBrowserDomainController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, apply: @escaping (String?) -> Void) -> AlertController { + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + + var dismissImpl: ((Bool) -> Void)? + var applyImpl: (() -> Void)? + + var inProgress = false + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + if !inProgress { + dismissImpl?(true) + apply(nil) + } + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: { + if !inProgress { + applyImpl?() + } + })] + + let contentNode = WebBrowserDomainAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions) + contentNode.complete = { + applyImpl?() + } + applyImpl = { [weak contentNode] in + guard let contentNode = contentNode else { + return + } + inProgress = true + contentNode.inputFieldNode.inProgress = true + contentNode.activityIndicator.isHidden = false + + let updatedLink = explicitUrl(contentNode.link) + if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true]) { + apply(updatedLink) + } else { + contentNode.animateError() + } + } + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in + controller?.theme = AlertControllerTheme(presentationData: presentationData) + contentNode?.inputFieldNode.updateTheme(presentationData.theme) + }) + controller.dismissed = { _ in + presentationDataDisposable.dispose() + } + dismissImpl = { [weak controller] animated in + contentNode.inputFieldNode.deactivateInput() + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift new file mode 100644 index 00000000000..869532b3a8e --- /dev/null +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift @@ -0,0 +1,347 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import TelegramCore +import AccountContext +import ItemListUI +import PhotoResources + +private enum RevealOptionKey: Int32 { + case delete +} + +final class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem { + let presentationData: ItemListPresentationData + let context: AccountContext + let title: String + let label: String + let icon: TelegramMediaImage? + let sectionId: ItemListSectionId + let style: ItemListStyle + let deleted: (() -> Void)? + + init( + presentationData: ItemListPresentationData, + context: AccountContext, + title: String, + label: String, + icon: TelegramMediaImage?, + sectionId: ItemListSectionId, + style: ItemListStyle, + deleted: (() -> Void)? + ) { + self.presentationData = presentationData + self.context = context + self.title = title + self.label = label + self.icon = icon + self.sectionId = sectionId + self.style = style + self.deleted = deleted + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = WebBrowserDomainExceptionItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? WebBrowserDomainExceptionItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } + + var selectable: Bool = false + + func selected(listView: ListView){ + } +} + +final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + let iconNode: TransformImageNode + let titleNode: TextNode + let labelNode: TextNode + + private let activateArea: AccessibilityAreaNode + + private var item: WebBrowserDomainExceptionItem? + private var layoutParams: ListViewItemLayoutParams? + + override public var canBeSelected: Bool { + return false + } + + var tag: ItemListItemTag? = nil + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.iconNode = TransformImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displaysAsynchronously = false + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + + self.labelNode = TextNode() + self.labelNode.isUserInteractionEnabled = false + + self.activateArea = AccessibilityAreaNode() + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.iconNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.labelNode) + + self.addSubnode(self.activateArea) + } + + func asyncLayout() -> (_ item: WebBrowserDomainExceptionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeLabelLayout = TextNode.asyncLayout(self.labelNode) + + let currentItem = self.item + + return { item, params, neighbors in + var updatedTheme: PresentationTheme? + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + let leftInset = 16.0 + params.leftInset + 46.0 + + let titleColor: UIColor = item.presentationData.theme.list.itemPrimaryTextColor + let labelColor: UIColor = item.presentationData.theme.list.itemAccentColor + + let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) + let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + + let maxTitleWidth: CGFloat = params.width - params.rightInset - 20.0 - leftInset + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: labelColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let verticalInset: CGFloat = 11.0 + let titleSpacing: CGFloat = 1.0 + + let height: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height + + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: height) + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: height) + insets = itemListNeighborsGroupedInsets(neighbors, params) + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + strongSelf.activateArea.accessibilityLabel = item.title + strongSelf.activateArea.accessibilityValue = item.label + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + } + + let iconSize = CGSize(width: 40.0, height: 40.0) + var imageSize = iconSize + if currentItem?.icon?.id != item.icon?.id, let icon = item.icon { + strongSelf.iconNode.setSignal(chatMessagePhoto(mediaBox: item.context.sharedContext.accountManager.mediaBox, userLocation: .other, photoReference: .standalone(media: icon))) + } + if let icon = item.icon, let dimensions = largestImageRepresentation(icon.representations)?.dimensions.cgSize { + imageSize = dimensions.aspectFilled(imageSize) + } + + let _ = titleApply() + let _ = labelApply() + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + if strongSelf.maskNode.supernode != nil { + strongSelf.maskNode.removeFromSupernode() + } + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + } + + var centralContentHeight: CGFloat = titleLayout.size.height + centralContentHeight += titleSpacing + centralContentHeight += labelLayout.size.height + + let titleFrame = CGRect(origin: CGPoint(x: leftInset + strongSelf.revealOffset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size) + strongSelf.titleNode.frame = titleFrame + + let labelFrame = CGRect(origin: CGPoint(x: leftInset + strongSelf.revealOffset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size) + strongSelf.labelNode.frame = labelFrame + + let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + 11.0 + strongSelf.revealOffset, y: floorToScreenPixels((contentSize.height - iconSize.height) / 2.0)), size: iconSize) + strongSelf.iconNode.frame = iconFrame + + strongSelf.iconNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 7.0), imageSize: imageSize, boundingSize: iconSize, intrinsicInsets: .zero))() + + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + + var revealOptions: [ItemListRevealOption] = [] + revealOptions.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)) + strongSelf.setRevealOptions((left: [], right: revealOptions)) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + super.updateRevealOffset(offset: offset, transition: transition) + + if let params = self.layoutParams { + let leftInset: CGFloat = 16.0 + params.leftInset + 46.0 + + var iconFrame = self.iconNode.frame + iconFrame.origin.x = params.leftInset + 11.0 + offset + transition.updateFrame(node: self.iconNode, frame: iconFrame) + + var titleFrame = self.titleNode.frame + titleFrame.origin.x = leftInset + offset + transition.updateFrame(node: self.titleNode, frame: titleFrame) + + var subtitleFrame = self.labelNode.frame + subtitleFrame.origin.x = leftInset + offset + transition.updateFrame(node: self.labelNode, frame: subtitleFrame) + } + } + + override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + if let item = self.item { + switch option.key { + case RevealOptionKey.delete.rawValue: + item.deleted?() + default: + break + } + } + self.setRevealOptionsOpened(false, animated: true) + self.revealOptionsInteractivelyClosed() + } +} diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift index 480f42c5f70..5dcd3e2263d 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift @@ -8,18 +8,20 @@ import TelegramPresentationData import ItemListUI import PhotoResources import OpenInExternalAppUI +import AccountContext +import AppBundle class WebBrowserItem: ListViewItem, ItemListItem { - let engine: TelegramEngine + let context: AccountContext let presentationData: ItemListPresentationData let title: String - let application: OpenInApplication + let application: OpenInApplication? let checked: Bool public let sectionId: ItemListSectionId let action: () -> Void - public init(engine: TelegramEngine, presentationData: ItemListPresentationData, title: String, application: OpenInApplication, checked: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { - self.engine = engine + public init(context: AccountContext, presentationData: ItemListPresentationData, title: String, application: OpenInApplication?, checked: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { + self.context = context self.presentationData = presentationData self.title = title self.application = application @@ -131,6 +133,7 @@ private final class WebBrowserItemNode: ListViewItemNode { let makeIconLayout = self.iconNode.asyncLayout() let currentItem = self.item + return { item, params, neighbors in let leftInset: CGFloat = params.leftInset + 16.0 + 43.0 @@ -140,18 +143,25 @@ private final class WebBrowserItemNode: ListViewItemNode { let imageApply = makeIconLayout(arguments) var updatedIconSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - if currentItem?.application != item.application { + if currentItem == nil { switch item.application { + case .none: + let icons = item.context.sharedContext.applicationBindings.getAvailableAlternateIcons() + let current = item.context.sharedContext.applicationBindings.getAlternateIconName() + let currentIcon = icons.first(where: { $0.name == current })?.imageName ?? "BlueIcon" + if let image = UIImage(named: currentIcon, in: getAppBundle(), compatibleWith: nil) { + updatedIconSignal = openInAppIcon(engine: item.context.engine, appIcon: .image(image: image)) + } case .safari: if let image = UIImage(bundleImageName: "Open In/Safari") { - updatedIconSignal = openInAppIcon(engine: item.engine, appIcon: .image(image: image)) + updatedIconSignal = openInAppIcon(engine: item.context.engine, appIcon: .image(image: image)) } case .maps: if let image = UIImage(bundleImageName: "Open In/Maps") { - updatedIconSignal = openInAppIcon(engine: item.engine, appIcon: .image(image: image)) + updatedIconSignal = openInAppIcon(engine: item.context.engine, appIcon: .image(image: image)) } case let .other(_, identifier, _, store): - updatedIconSignal = openInAppIcon(engine: item.engine, appIcon: .resource(resource: OpenInAppIconResource(appStoreId: identifier, store: store))) + updatedIconSignal = openInAppIcon(engine: item.context.engine, appIcon: .resource(resource: OpenInAppIconResource(appStoreId: identifier, store: store))) } } diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift index d35202da747..faf85ab0ea9 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift @@ -6,41 +6,124 @@ import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences +import PresentationDataUtils import ItemListUI import AccountContext import OpenInExternalAppUI +import ItemListPeerActionItem +import UndoUI +import WebKit +import LinkPresentation +import CoreServices +import PersistentStringHash +import UrlHandling private final class WebBrowserSettingsControllerArguments { let context: AccountContext let updateDefaultBrowser: (String?) -> Void + let clearCookies: () -> Void + let clearCache: () -> Void + let addException: () -> Void + let removeException: (String) -> Void + let clearExceptions: () -> Void - init(context: AccountContext, updateDefaultBrowser: @escaping (String?) -> Void) { + init( + context: AccountContext, + updateDefaultBrowser: @escaping (String?) -> Void, + clearCookies: @escaping () -> Void, + clearCache: @escaping () -> Void, + addException: @escaping () -> Void, + removeException: @escaping (String) -> Void, + clearExceptions: @escaping () -> Void + ) { self.context = context self.updateDefaultBrowser = updateDefaultBrowser + self.clearCookies = clearCookies + self.clearCache = clearCache + self.addException = addException + self.removeException = removeException + self.clearExceptions = clearExceptions } } private enum WebBrowserSettingsSection: Int32 { case browsers + case clearCookies + case exceptions } private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { case browserHeader(PresentationTheme, String) - case browser(PresentationTheme, String, OpenInApplication, String?, Bool, Int32) + case browser(PresentationTheme, String, OpenInApplication?, String?, Bool, Int32) + + case clearCookies(PresentationTheme, String) + case clearCache(PresentationTheme, String) + case clearCookiesInfo(PresentationTheme, String) + + case exceptionsHeader(PresentationTheme, String) + case exceptionsAdd(PresentationTheme, String) + case exception(Int32, PresentationTheme, WebBrowserException) + case exceptionsClear(PresentationTheme, String) + case exceptionsInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { case .browserHeader, .browser: return WebBrowserSettingsSection.browsers.rawValue + case .clearCookies, .clearCache, .clearCookiesInfo: + return WebBrowserSettingsSection.clearCookies.rawValue + case .exceptionsHeader, .exceptionsAdd, .exception, .exceptionsClear, .exceptionsInfo: + return WebBrowserSettingsSection.exceptions.rawValue + } + } + + var stableId: UInt64 { + switch self { + case .browserHeader: + return 0 + case let .browser(_, _, _, _, _, index): + return UInt64(1 + index) + case .clearCookies: + return 102 + case .clearCache: + return 103 + case .clearCookiesInfo: + return 104 + case .exceptionsHeader: + return 105 + case .exceptionsAdd: + return 106 + case let .exception(_, _, exception): + return 2000 + exception.domain.persistentHashValue + case .exceptionsClear: + return 1000 + case .exceptionsInfo: + return 1001 } } - var stableId: Int32 { + var sortId: Int32 { switch self { case .browserHeader: return 0 case let .browser(_, _, _, _, _, index): return 1 + index + case .clearCookies: + return 102 + case .clearCache: + return 103 + case .clearCookiesInfo: + return 104 + case .exceptionsHeader: + return 105 + case .exceptionsAdd: + return 106 + case let .exception(index, _, _): + return 107 + index + case .exceptionsClear: + return 1000 + case .exceptionsInfo: + return 1001 } } @@ -58,11 +141,59 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { } else { return false } + case let .clearCookies(lhsTheme, lhsText): + if case let .clearCookies(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .clearCache(lhsTheme, lhsText): + if case let .clearCache(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .clearCookiesInfo(lhsTheme, lhsText): + if case let .clearCookiesInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .exceptionsHeader(lhsTheme, lhsText): + if case let .exceptionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .exception(lhsIndex, lhsTheme, lhsException): + if case let .exception(rhsIndex, rhsTheme, rhsException) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsException == rhsException { + return true + } else { + return false + } + case let .exceptionsAdd(lhsTheme, lhsText): + if case let .exceptionsAdd(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .exceptionsClear(lhsTheme, lhsText): + if case let .exceptionsClear(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .exceptionsInfo(lhsTheme, lhsText): + if case let .exceptionsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } } } static func <(lhs: WebBrowserSettingsControllerEntry, rhs: WebBrowserSettingsControllerEntry) -> Bool { - return lhs.stableId < rhs.stableId + return lhs.sortId < rhs.sortId } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { @@ -71,46 +202,311 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { case let .browserHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .browser(_, title, application, identifier, selected, _): - return WebBrowserItem(engine: arguments.context.engine, presentationData: presentationData, title: title, application: application, checked: selected, sectionId: self.section) { + return WebBrowserItem(context: arguments.context, presentationData: presentationData, title: title, application: application, checked: selected, sectionId: self.section) { arguments.updateDefaultBrowser(identifier) } + case let .clearCookies(_, text): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.accentDeleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: { + arguments.clearCookies() + }) + case let .clearCache(_, text): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.accentDeleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: { + arguments.clearCache() + }) + case let .clearCookiesInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .exceptionsHeader(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .exception(_, _, exception): + return WebBrowserDomainExceptionItem(presentationData: presentationData, context: arguments.context, title: exception.title, label: exception.domain, icon: exception.icon, sectionId: self.section, style: .blocks, deleted: { + arguments.removeException(exception.domain) + }) + case let .exceptionsAdd(_, text): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: { + arguments.addException() + }) + case let .exceptionsClear(_, text): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.deleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: { + arguments.clearExceptions() + }) + case let .exceptionsInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } -private func webBrowserSettingsControllerEntries(context: AccountContext, presentationData: PresentationData, selectedBrowser: String?) -> [WebBrowserSettingsControllerEntry] { +private func webBrowserSettingsControllerEntries(context: AccountContext, presentationData: PresentationData, settings: WebBrowserSettings) -> [WebBrowserSettingsControllerEntry] { var entries: [WebBrowserSettingsControllerEntry] = [] let options = availableOpenInOptions(context: context, item: .url(url: "http://telegram.org")) - entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_DefaultBrowser)) - entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_InAppSafari, .safari, nil, selectedBrowser == nil, 0)) + entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_OpenLinksIn_Title)) + entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, settings.defaultWebBrowser == nil, 0)) var index: Int32 = 1 for option in options { - entries.append(.browser(presentationData.theme, option.title, option.application, option.identifier, option.identifier == selectedBrowser, index)) + entries.append(.browser(presentationData.theme, option.title, option.application, option.identifier, option.identifier == settings.defaultWebBrowser, index)) index += 1 } + if settings.defaultWebBrowser == nil { + entries.append(.clearCookies(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies)) +// entries.append(.clearCache(presentationData.theme, presentationData.strings.WebBrowser_ClearCache)) + entries.append(.clearCookiesInfo(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies_Info)) + + entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Title)) + entries.append(.exceptionsAdd(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_AddException)) + + var exceptionIndex: Int32 = 0 + for exception in settings.exceptions.reversed() { + entries.append(.exception(exceptionIndex, presentationData.theme, exception)) + exceptionIndex += 1 + } + + if !settings.exceptions.isEmpty { + entries.append(.exceptionsClear(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Clear)) + } + + entries.append(.exceptionsInfo(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Info)) + } + return entries } public func webBrowserSettingsController(context: AccountContext) -> ViewController { - let arguments = WebBrowserSettingsControllerArguments(context: context, updateDefaultBrowser: { identifier in - let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { $0.withUpdatedDefaultWebBrowser(identifier) }).start() - }) + var clearCookiesImpl: (() -> Void)? + var clearCacheImpl: (() -> Void)? + var addExceptionImpl: (() -> Void)? + var removeExceptionImpl: ((String) -> Void)? + var clearExceptionsImpl: (() -> Void)? + + let arguments = WebBrowserSettingsControllerArguments( + context: context, + updateDefaultBrowser: { identifier in + let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { + $0.withUpdatedDefaultWebBrowser(identifier) + }).start() + }, + clearCookies: { + clearCookiesImpl?() + }, + clearCache: { + clearCacheImpl?() + }, + addException: { + addExceptionImpl?() + }, + removeException: { domain in + removeExceptionImpl?(domain) + }, + clearExceptions: { + clearExceptionsImpl?() + } + ) - let signal = combineLatest(context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings])) + let previousSettings = Atomic(value: nil) + + let signal = combineLatest( + context.sharedContext.presentationData, + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings]) + ) |> deliverOnMainQueue |> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) ?? WebBrowserSettings.defaultSettings + let previousSettings = previousSettings.swap(settings) + + var animateChanges = false + if let previousSettings { + if previousSettings.defaultWebBrowser != settings.defaultWebBrowser { + animateChanges = true + } + if previousSettings.exceptions.count != settings.exceptions.count { + animateChanges = true + } + } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.WebBrowser_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: webBrowserSettingsControllerEntries(context: context, presentationData: presentationData, selectedBrowser: settings.defaultWebBrowser), style: .blocks, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: webBrowserSettingsControllerEntries(context: context, presentationData: presentationData, settings: settings), style: .blocks, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } let controller = ItemListController(context: context, state: signal) + + clearCookiesImpl = { [weak controller] in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let alertController = textAlertController( + context: context, + updatedPresentationData: nil, + title: nil, + text: presentationData.strings.WebBrowser_ClearCookies_ClearConfirmation_Text, + actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_ClearCookies_ClearConfirmation_Clear, action: { + WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeCookies, WKWebsiteDataTypeLocalStorage, WKWebsiteDataTypeSessionStorage], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{}) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + controller?.present(UndoOverlayController( + presentationData: presentationData, + content: .info( + title: nil, + text: presentationData.strings.WebBrowser_ClearCookies_Succeed, + timeout: nil, + customUndoText: nil + ), + elevatedLayout: false, + position: .bottom, + action: { _ in return false }), in: .current + ) + }) + ] + ) + controller?.present(alertController, in: .window(.root)) + } + + clearCacheImpl = { [weak controller] in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let alertController = textAlertController( + context: context, + updatedPresentationData: nil, + title: nil, + text: presentationData.strings.WebBrowser_ClearCache_ClearConfirmation_Text, + actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_ClearCache_ClearConfirmation_Clear, action: { + WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{}) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + controller?.present(UndoOverlayController( + presentationData: presentationData, + content: .info( + title: nil, + text: presentationData.strings.WebBrowser_ClearCache_Succeed, + timeout: nil, + customUndoText: nil + ), + elevatedLayout: false, + position: .bottom, + action: { _ in return false }), in: .current + ) + }) + ] + ) + controller?.present(alertController, in: .window(.root)) + } + + addExceptionImpl = { [weak controller] in + var dismissImpl: (() -> Void)? + let linkController = webBrowserDomainController(context: context, apply: { url in + if let url { + let _ = (fetchDomainExceptionInfo(context: context, url: url) + |> deliverOnMainQueue).startStandalone(next: { newException in + let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in + var currentExceptions = currentSettings.exceptions + for exception in currentExceptions { + if exception.domain == newException.domain { + return currentSettings + } + } + currentExceptions.append(newException) + return currentSettings.withUpdatedExceptions(currentExceptions) + }).start() + dismissImpl?() + }) + } + }) + dismissImpl = { [weak linkController] in + linkController?.view.endEditing(true) + linkController?.dismissAnimated() + } + controller?.present(linkController, in: .window(.root)) + } + + removeExceptionImpl = { domain in + let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in + let updatedExceptions = currentSettings.exceptions.filter { $0.domain != domain } + return currentSettings.withUpdatedExceptions(updatedExceptions) + }).start() + } + + clearExceptionsImpl = { [weak controller] in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let alertController = textAlertController( + context: context, + updatedPresentationData: nil, + title: nil, + text: presentationData.strings.WebBrowser_Exceptions_ClearConfirmation_Text, + actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_Exceptions_ClearConfirmation_Clear, action: { + let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in + return currentSettings.withUpdatedExceptions([]) + }).start() + }) + ] + ) + controller?.present(alertController, in: .window(.root)) + } + return controller } + +private func fetchDomainExceptionInfo(context: AccountContext, url: String) -> Signal { + let (domain, domainUrl) = cleanDomain(url: url) + if #available(iOS 13.0, *), let url = URL(string: domainUrl) { + return Signal { subscriber in + let metadataProvider = LPMetadataProvider() + metadataProvider.shouldFetchSubresources = true + metadataProvider.startFetchingMetadata(for: url, completionHandler: { metadata, _ in + let completeWithImage: (Data?) -> Void = { imageData in + var image: TelegramMediaImage? + if let imageData, let parsedImage = UIImage(data: imageData) { + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: imageData) + image = TelegramMediaImage( + imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), + representations: [ + TelegramMediaImageRepresentation( + dimensions: PixelDimensions(width: Int32(parsedImage.size.width), height: Int32(parsedImage.size.height)), + resource: resource, + progressiveSizes: [], + immediateThumbnailData: nil, + hasVideo: false, + isPersonal: false + ) + ], + immediateThumbnailData: nil, + reference: nil, + partialReference: nil, + flags: [] + ) + } + + let title = metadata?.value(forKey: "_siteName") as? String ?? metadata?.title + subscriber.putNext(WebBrowserException(domain: domain, title: title ?? domain, icon: image)) + subscriber.putCompletion() + } + + if let imageProvider = metadata?.iconProvider { + imageProvider.loadFileRepresentation(forTypeIdentifier: kUTTypeImage as String, completionHandler: { imageUrl, _ in + guard let imageUrl, let imageData = try? Data(contentsOf: imageUrl) else { + completeWithImage(nil) + return + } + completeWithImage(imageData) + }) + } else { + completeWithImage(nil) + } + }) + return ActionDisposable { + metadataProvider.cancel() + } + } + } else { + return .single(WebBrowserException(domain: domain, title: domain, icon: nil)) + } +} diff --git a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift index a118eb72839..52c56e8455e 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift @@ -145,7 +145,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { var peers = SimpleDictionary() let messages = SimpleDictionary() - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: item.peerName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: item.peerName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) let forwardInfo = MessageForwardInfo(author: item.linkEnabled ? peers[peerId] : nil, source: nil, sourceMessageId: nil, date: 0, authorSignature: item.linkEnabled ? nil : item.peerName, psaType: nil, flags: []) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift index 4d02db8394a..33ef3ccc3ab 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift @@ -842,10 +842,12 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present } if case .phoneNumber = kind, state.setting == .nobody { - entries.append(.phoneDiscoveryHeader(presentationData.theme, presentationData.strings.PrivacyPhoneNumberSettings_DiscoveryHeader)) - entries.append(.phoneDiscoveryEverybody(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenEverybody, state.phoneDiscoveryEnabled != false)) - entries.append(.phoneDiscoveryMyContacts(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenContacts, state.phoneDiscoveryEnabled == false)) - entries.append(.phoneDiscoveryInfo(presentationData.theme, state.phoneDiscoveryEnabled != false ? presentationData.strings.PrivacyPhoneNumberSettings_CustomPublicLink("+\(phoneNumber)").string : presentationData.strings.PrivacyPhoneNumberSettings_CustomDisabledHelp, phoneLink)) + if state.phoneDiscoveryEnabled == false { + entries.append(.phoneDiscoveryHeader(presentationData.theme, presentationData.strings.PrivacyPhoneNumberSettings_DiscoveryHeader)) + entries.append(.phoneDiscoveryEverybody(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenEverybody, state.phoneDiscoveryEnabled != false)) + entries.append(.phoneDiscoveryMyContacts(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenContacts, state.phoneDiscoveryEnabled == false)) + entries.append(.phoneDiscoveryInfo(presentationData.theme, state.phoneDiscoveryEnabled != false ? presentationData.strings.PrivacyPhoneNumberSettings_CustomPublicLink("+\(phoneNumber)").string : presentationData.strings.PrivacyPhoneNumberSettings_CustomDisabledHelp, phoneLink)) + } } if case .voiceMessages = kind, !isPremium { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift index 81ecb8434b1..d1202a9c8e0 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift @@ -175,7 +175,7 @@ private enum SelectivePrivacyPeersEntry: ItemListNodeEntry { switch self { case let .premiumUsersItem(editing, enabled): let peer: EnginePeer = .user(TelegramUser( - id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(1)), accessHash: nil, firstName: presentationData.strings.PrivacySettings_CategoryPremiumUsers, 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)) + id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(1)), accessHash: nil, firstName: presentationData.strings.PrivacySettings_CategoryPremiumUsers, 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)) return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: .firstLast, context: arguments.context, peer: peer, customAvatarIcon: premiumAvatarIcon, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { }, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 2ce353ed872..94e9ed572db 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -312,14 +312,14 @@ 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)) - 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)) - 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)) + 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 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)) - 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)) + 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 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)) + 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 @@ -435,8 +435,8 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView let otherPeerId = self.context.account.peerId var peers = SimpleDictionary() var messages = SimpleDictionary() - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) - peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) + peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 4dfabf4ada8..336cf3e8b6f 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -461,15 +461,15 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { let chatListPresentationData = ChatListPresentationData(theme: self.previewTheme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) - 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)) - 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)) - 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)) + 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 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)) - 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)) + 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 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)) - 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)) + 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)) let timestamp = self.referenceTimestamp @@ -590,8 +590,8 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { let otherPeerId = self.context.account.peerId var peers = SimpleDictionary() var messages = SimpleDictionary() - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) - peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) + peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) var sampleMessages: [Message] = [] diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift index b431aff3f38..27d2fb7cab9 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift @@ -163,11 +163,11 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) if let (author, text) = messageItem.reply { - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: author, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil) + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: author, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) } - let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, 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) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, 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) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, 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: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)) } diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index 831a1c509a5..b3fb9e33ed9 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -550,7 +550,13 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate added = true } - strongSelf.contentNode?.setEnsurePeerVisibleOnLayout(peer.peerId) + if openedTopicList { + Queue.mainQueue().after(0.4) { + strongSelf.contentNode?.setEnsurePeerVisibleOnLayout(peer.peerId) + } + } else { + strongSelf.contentNode?.setEnsurePeerVisibleOnLayout(peer.peerId) + } } if search && added { @@ -718,7 +724,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate if let searchContentNode = strongSelf.contentNode as? ShareSearchContainerNode { searchContentNode.setDidBeginDragging(nil) searchContentNode.setContentOffsetUpdated(nil) - let scrollDelta = topicsContentNode.contentGridNode.scrollView.contentOffset.y - searchContentNode.contentGridNode.scrollView.contentOffset.y + let scrollDelta = topicsContentNode.contentGridNode.scrollView.contentOffset.y - searchContentNode.effectiveGridNode.scrollView.contentOffset.y if let sourceFrame = searchContentNode.animateOut(peerId: peer.peerId, scrollDelta: scrollDelta) { topicsContentNode.animateIn(sourceFrame: sourceFrame, scrollDelta: scrollDelta) } @@ -767,9 +773,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate searchContentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in self?.contentNodeOffsetUpdated(contentOffset, transition: transition) }) - self.contentNodeOffsetUpdated(searchContentNode.contentGridNode.scrollView.contentOffset.y, transition: .animated(duration: 0.4, curve: .spring)) + self.contentNodeOffsetUpdated(searchContentNode.effectiveGridNode.scrollView.contentOffset.y, transition: .animated(duration: 0.4, curve: .spring)) - let scrollDelta = topicsContentNode.contentGridNode.scrollView.contentOffset.y - searchContentNode.contentGridNode.scrollView.contentOffset.y + let scrollDelta = topicsContentNode.contentGridNode.scrollView.contentOffset.y - searchContentNode.effectiveGridNode.scrollView.contentOffset.y if let targetFrame = searchContentNode.animateIn(peerId: peerId, scrollDelta: scrollDelta) { topicsContentNode.animateOut(targetFrame: targetFrame, scrollDelta: scrollDelta, completion: { [weak self] in if let topicsContentNode = self?.topicsContentNode { diff --git a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift index 1d6c98dede7..292cc01841b 100644 --- a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift +++ b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift @@ -279,7 +279,7 @@ public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareConte if let postbox, let mediaManager = environment.mediaManager, let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) { let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black) - let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [], preloadSize: nil)]) + let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [], preloadSize: nil, coverTime: nil)]) let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil) diff --git a/submodules/ShareController/Sources/ShareSearchContainerNode.swift b/submodules/ShareController/Sources/ShareSearchContainerNode.swift index ee36f60e4b8..0048043963f 100644 --- a/submodules/ShareController/Sources/ShareSearchContainerNode.swift +++ b/submodules/ShareController/Sources/ShareSearchContainerNode.swift @@ -106,6 +106,7 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable { let requiresPremiumForMessaging: Bool let theme: PresentationTheme let strings: PresentationStrings + let isGlobal: Bool var stableId: Int64 { if let peer = self.peer { @@ -131,6 +132,9 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable { if lhs.theme !== rhs.theme { return false } + if lhs.isGlobal != rhs.isGlobal { + return false + } return true } @@ -139,9 +143,13 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable { } func item(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem { -// let item: ShareControllerPeerGridItem.ShareItem -// item = self.peer.flatMap { .peer(peer: $0, presence: self.presence, topicId: nil, threadData: nil) } - return ShareControllerPeerGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, item: self.peer.flatMap({ .peer(peer: $0, presence: self.presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: self.requiresPremiumForMessaging) }), controllerInteraction: interfaceInteraction, search: true) + let sectionTitle: String? + if self.isGlobal { + sectionTitle = self.strings.Contacts_GlobalSearch.uppercased() + } else { + sectionTitle = nil + } + return ShareControllerPeerGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, item: self.peer.flatMap({ .peer(peer: $0, presence: self.presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: self.requiresPremiumForMessaging) }), controllerInteraction: interfaceInteraction, sectionTitle: sectionTitle, search: true) } } @@ -190,6 +198,14 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { let contentGridNode: GridNode private let recentGridNode: GridNode + var effectiveGridNode: GridNode { + if !self.recentGridNode.isHidden { + return self.recentGridNode + } else { + return self.contentGridNode + } + } + private let contentSeparatorNode: ASDisplayNode private let searchNode: ShareSearchBarNode private let cancelButtonNode: HighlightableButtonNode @@ -345,7 +361,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { if strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery) { if !existingPeerIds.contains(accountPeer.id) { existingPeerIds.insert(accountPeer.id) - entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(accountPeer)), presence: nil, requiresPremiumForMessaging: false, theme: theme, strings: strings)) + entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(accountPeer)), presence: nil, requiresPremiumForMessaging: false, theme: theme, strings: strings, isGlobal: false)) index += 1 } } @@ -354,7 +370,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != accountPeer.id { if !existingPeerIds.contains(renderedPeer.peerId) && canSendMessagesToPeer(peer) { existingPeerIds.insert(renderedPeer.peerId) - entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(renderedPeer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, theme: theme, strings: strings)) + entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(renderedPeer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, theme: theme, strings: strings, isGlobal: false)) index += 1 } } @@ -364,7 +380,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { if foundRemotePeers.2 { isPlaceholder = true for _ in 0 ..< 4 { - entries.append(ShareSearchPeerEntry(index: index, peer: nil, presence: nil, requiresPremiumForMessaging: false, theme: theme, strings: strings)) + entries.append(ShareSearchPeerEntry(index: index, peer: nil, presence: nil, requiresPremiumForMessaging: false, theme: theme, strings: strings, isGlobal: false)) index += 1 } } else { @@ -372,7 +388,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { let peer = foundPeer.peer if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) { existingPeerIds.insert(peer.id) - entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(foundPeer.peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, theme: theme, strings: strings)) + entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(foundPeer.peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, theme: theme, strings: strings, isGlobal: false)) index += 1 } } @@ -381,7 +397,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { let peer = foundPeer.peer if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) { existingPeerIds.insert(peer.id) - entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, theme: theme, strings: strings)) + entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, theme: theme, strings: strings, isGlobal: true)) index += 1 } } @@ -759,14 +775,16 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { self.cancelButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.cancelButtonNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.contentGridNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + let gridNode = self.effectiveGridNode + + gridNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) if let targetFrame = self.frameForPeerId(peerId), let (size, bottomInset) = self.validLayout { let clippedNode = ASDisplayNode() clippedNode.clipsToBounds = true clippedNode.cornerRadius = 16.0 clippedNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.searchNode.frame.minY - 15.0), size: CGSize(width: size.width, height: size.height - bottomInset)) - self.contentGridNode.view.superview?.insertSubview(clippedNode.view, aboveSubview: self.contentGridNode.view) + gridNode.view.superview?.insertSubview(clippedNode.view, aboveSubview: gridNode.view) clippedNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) @@ -779,9 +797,8 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { maskView.addSubview(maskImageView) clippedNode.view.mask = maskView - - self.contentGridNode.alpha = 1.0 - self.contentGridNode.forEachItemNode { itemNode in + gridNode.alpha = 1.0 + gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ShareControllerPeerGridItemNode, itemNode.peerId == peerId { itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, removeOnCompletion: false) itemNode.layer.animateScale(from: 1.35, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak clippedNode] _ in @@ -820,14 +837,16 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { self.cancelButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.cancelButtonNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.contentGridNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + let gridNode = self.effectiveGridNode + + gridNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) if let sourceFrame = self.frameForPeerId(peerId), let (size, bottomInset) = self.validLayout { let clippedNode = ASDisplayNode() clippedNode.clipsToBounds = true clippedNode.cornerRadius = 16.0 clippedNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.searchNode.frame.minY - 15.0), size: CGSize(width: size.width, height: size.height - bottomInset)) - self.contentGridNode.view.superview?.insertSubview(clippedNode.view, aboveSubview: self.contentGridNode.view) + gridNode.view.superview?.insertSubview(clippedNode.view, aboveSubview: gridNode.view) clippedNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) @@ -840,7 +859,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { maskView.addSubview(maskImageView) clippedNode.view.mask = maskView - self.contentGridNode.forEachItemNode { itemNode in + gridNode.forEachItemNode { itemNode in if let snapshotView = itemNode.view.snapshotView(afterScreenUpdates: false) { snapshotView.frame = itemNode.view.convert(itemNode.bounds, to: clippedNode.view) clippedNode.view.addSubview(snapshotView) @@ -862,7 +881,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { clippedNode?.view.removeFromSuperview() }) - self.contentGridNode.alpha = 0.0 + gridNode.alpha = 0.0 return sourceFrame } else { diff --git a/submodules/ShareItems/Sources/ShareItems.swift b/submodules/ShareItems/Sources/ShareItems.swift index 3ff8e54ca65..71ae6be4232 100644 --- a/submodules/ShareItems/Sources/ShareItems.swift +++ b/submodules/ShareItems/Sources/ShareItems.swift @@ -144,7 +144,7 @@ private func preparedShareItem(postbox: Postbox, network: Network, to peerId: Pe let estimatedSize = TGMediaVideoConverter.estimatedSize(for: preset, duration: finalDuration, hasAudio: true) let resource = LocalFileVideoMediaResource(randomId: Int64.random(in: Int64.min ... Int64.max), path: asset.url.path, adjustments: resourceAdjustments) - return standaloneUploadedFile(postbox: postbox, network: network, peerId: peerId, text: "", source: .resource(.standalone(resource: resource)), mimeType: "video/mp4", attributes: [.Video(duration: finalDuration, size: PixelDimensions(width: Int32(finalDimensions.width), height: Int32(finalDimensions.height)), flags: flags, preloadSize: nil)], hintFileIsLarge: estimatedSize > 10 * 1024 * 1024) + return standaloneUploadedFile(postbox: postbox, network: network, peerId: peerId, text: "", source: .resource(.standalone(resource: resource)), mimeType: "video/mp4", attributes: [.Video(duration: finalDuration, size: PixelDimensions(width: Int32(finalDimensions.width), height: Int32(finalDimensions.height)), flags: flags, preloadSize: nil, coverTime: nil)], hintFileIsLarge: estimatedSize > 10 * 1024 * 1024) |> mapError { _ -> PreparedShareItemError in return .generic } @@ -210,7 +210,7 @@ private func preparedShareItem(postbox: Postbox, network: Network, to peerId: Pe let mimeType: String if converted { mimeType = "video/mp4" - attributes = [.Video(duration: duration, size: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height)), flags: [.supportsStreaming], preloadSize: nil), .Animated, .FileName(fileName: "animation.mp4")] + attributes = [.Video(duration: duration, size: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height)), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil), .Animated, .FileName(fileName: "animation.mp4")] } else { mimeType = "animation/gif" attributes = [.ImageSize(size: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height))), .Animated, .FileName(fileName: fileName ?? "animation.gif")] diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index 8e0a317db89..9b8015eaf4a 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -38,6 +38,7 @@ public protocol SparseItemGridBinding: AnyObject { func unbindLayer(layer: SparseItemGridLayer) func scrollerTextForTag(tag: Int32) -> String? func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal + func reorderIfPossible(item: SparseItemGrid.Item, toIndex: Int) func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) func onTagTap() func didScroll() @@ -189,6 +190,10 @@ public final class SparseItemGrid: ASDisplayNode { open var holeAnchor: HoleAnchor { preconditionFailure() } + + open var isReorderable: Bool { + return false + } public init() { } @@ -376,6 +381,48 @@ public final class SparseItemGrid: ASDisplayNode { } } } + + var position: CGPoint { + get { + return self.displayLayer.position + } set(value) { + if let layer = self.layer { + layer.position = value + } else if let view = self.view { + view.center = value + } else { + preconditionFailure() + } + } + } + + var bounds: CGRect { + get { + return self.displayLayer.bounds + } set(value) { + if let layer = self.layer { + layer.bounds = value + } else if let view = self.view { + view.bounds = value + } else { + preconditionFailure() + } + } + } + + var transform: CATransform3D { + get { + return self.displayLayer.transform + } set(value) { + if let layer = self.layer { + layer.transform = value + } else if let view = self.view { + view.layer.transform = value + } else { + preconditionFailure() + } + } + } var needsShimmer: Bool { if let layer = self.layer { @@ -415,7 +462,7 @@ public final class SparseItemGrid: ASDisplayNode { self.itemSpacing = 1.0 let itemsPerRow: CGFloat - if containerLayout.fixedItemAspect != nil && itemCount <= 2 { + if containerLayout.fixedItemAspect != nil && itemCount <= 2 && containerLayout.adjustForSmallCount { itemsPerRow = 2.0 centerItems = itemCount == 1 } else { @@ -485,6 +532,8 @@ public final class SparseItemGrid: ASDisplayNode { var items: Items? var visibleItems: [AnyHashable: VisibleItem] = [:] var visiblePlaceholders: [SparseItemGridShimmerLayer] = [] + + private var reorderingItem: (id: AnyHashable, initialPosition: CGPoint, position: CGPoint)? private var scrollingArea: SparseItemGridScrollingArea? private var currentScrollingTag: Int32? @@ -492,6 +541,8 @@ public final class SparseItemGrid: ASDisplayNode { private var ignoreScrolling: Bool = false private var isFastScrolling: Bool = false + + private var isReordering: Bool = false private var previousScrollOffset: CGFloat = 0.0 var coveringInsetOffset: CGFloat = 0.0 @@ -499,6 +550,11 @@ public final class SparseItemGrid: ASDisplayNode { var offset: CGFloat { return self.scrollView.contentOffset.y } + + var contentBottomOffset: CGFloat { + let bottomInset = self.layout?.containerLayout.insets.bottom ?? 0.0 + return -self.scrollView.contentOffset.y + self.scrollView.contentSize.height - bottomInset + } let coveringOffsetUpdated: (Viewport, ContainedViewLayoutTransition) -> Void let offsetUpdated: (Viewport, ContainedViewLayoutTransition) -> Void @@ -532,16 +588,68 @@ public final class SparseItemGrid: ASDisplayNode { self.view.addSubview(self.scrollView) } - func update(containerLayout: ContainerLayout, items: Items, restoreScrollPosition: (y: CGFloat, index: Int)?, synchronous: SparseItemGrid.Synchronous) { + func update(containerLayout: ContainerLayout, items: Items, restoreScrollPosition: (y: CGFloat, index: Int)?, synchronous: SparseItemGrid.Synchronous, transition: ComponentTransition) { if self.layout?.containerLayout != containerLayout || self.items !== items { self.layout = Layout(containerLayout: containerLayout, zoomLevel: self.zoomLevel, itemCount: items.count) self.items = items - self.updateVisibleItems(resetScrolling: true, synchronous: synchronous, restoreScrollPosition: restoreScrollPosition) + self.updateVisibleItems(resetScrolling: true, synchronous: synchronous, restoreScrollPosition: restoreScrollPosition, transition: transition) self.snapCoveringInsetOffset(animated: false) } } + + func setReordering(isReordering: Bool) { + if self.isReordering != isReordering { + self.isReordering = isReordering + + self.updateVisibleItems(resetScrolling: true, synchronous: .semi, restoreScrollPosition: nil, transition: .spring(duration: 0.4)) + } + } + + func setReorderingItem(item: SparseItemGridDisplayItem?) { + var mappedItem: (AnyHashable, VisibleItem)? + if let item, let itemLayer = item.layer { + for (id, visibleItem) in self.visibleItems { + if visibleItem.layer === itemLayer { + mappedItem = (id, visibleItem) + break + } + } + } + + if self.reorderingItem?.id != mappedItem?.0 { + if let (id, visibleItem) = mappedItem, let itemLayer = visibleItem.layer { + self.scrollView.layer.addSublayer(itemLayer) + self.reorderingItem = (id, itemLayer.position, itemLayer.position) + } else { + self.reorderingItem = nil + } + self.updateVisibleItems(resetScrolling: true, synchronous: .semi, restoreScrollPosition: nil, transition: .spring(duration: 0.4)) + } + } + + func moveReorderingItem(distance: CGPoint) { + if let (id, initialPosition, _) = self.reorderingItem { + let targetPosition = CGPoint(x: initialPosition.x + distance.x, y: initialPosition.y + distance.y) + self.reorderingItem = (id, initialPosition, targetPosition) + self.updateVisibleItems(resetScrolling: true, synchronous: .semi, restoreScrollPosition: nil, transition: .immediate) + + if let items = self.items, let visibleReorderingItem = self.visibleItems[id] { + for (visibleId, visibleItem) in self.visibleItems { + if visibleItem === visibleReorderingItem { + continue + } + if visibleItem.frame.contains(targetPosition) { + if let item = items.items.first(where: { $0.id == id }), let targetItem = items.items.first(where: { $0.id == visibleId }) { + items.itemBinding.reorderIfPossible(item: item, toIndex: targetItem.index) + } + break + } + } + } + } + } @objc func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.items?.itemBinding.didScroll() @@ -554,7 +662,7 @@ public final class SparseItemGrid: ASDisplayNode { @objc func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { - self.updateVisibleItems(resetScrolling: false, synchronous: .full, restoreScrollPosition: nil) + self.updateVisibleItems(resetScrolling: false, synchronous: .full, restoreScrollPosition: nil, transition: .immediate) if let layout = self.layout, let _ = self.items { let offset = scrollView.contentOffset.y @@ -916,10 +1024,10 @@ public final class SparseItemGrid: ASDisplayNode { } func updateShimmerColors() { - self.updateVisibleItems(resetScrolling: false, synchronous: .none, restoreScrollPosition: nil) + self.updateVisibleItems(resetScrolling: false, synchronous: .none, restoreScrollPosition: nil, transition: .immediate) } - private func updateVisibleItems(resetScrolling: Bool, synchronous: SparseItemGrid.Synchronous, restoreScrollPosition: (y: CGFloat, index: Int)?) { + private func updateVisibleItems(resetScrolling: Bool, synchronous: SparseItemGrid.Synchronous, restoreScrollPosition: (y: CGFloat, index: Int)?, transition: ComponentTransition) { guard let layout = self.layout, let items = self.items else { return } @@ -980,23 +1088,32 @@ public final class SparseItemGrid: ASDisplayNode { let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count) if visibleRange.maxIndex >= visibleRange.minIndex { - for index in visibleRange.minIndex ... visibleRange.maxIndex { + let processItemAtIndex: (Int) -> Void = { index in if let item = items.item(at: index) { - let itemFrame = layout.frame(at: index) + var itemFrame = layout.frame(at: index) let itemLayer: VisibleItem + var isNewlyAdded = false if let current = self.visibleItems[item.id] { itemLayer = current updateLayers.append((itemLayer, index)) } else { + isNewlyAdded = true itemLayer = VisibleItem(layer: items.itemBinding.createLayer(item: item), view: items.itemBinding.createView()) + + itemLayer.layer?.masksToBounds = true + self.visibleItems[item.id] = itemLayer bindItems.append(item) bindLayers.append(itemLayer) if let layer = itemLayer.layer { - self.scrollView.layer.addSublayer(layer) + if let reorderingItem = self.reorderingItem, let visibleReorderingItem = self.visibleItems[reorderingItem.id] { + self.scrollView.layer.insertSublayer(layer, below: visibleReorderingItem.layer) + } else { + self.scrollView.layer.addSublayer(layer) + } } else if let view = itemLayer.view { self.scrollView.addSubview(view) } @@ -1038,9 +1155,55 @@ public final class SparseItemGrid: ASDisplayNode { validIds.insert(item.id) - itemLayer.frame = itemFrame - if let blurLayer = itemLayer.blurLayer { - blurLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: itemFrame.minY), size: CGSize(width: layout.containerLayout.size.width, height: itemFrame.height)) + var itemScale: CGFloat + let itemCornerRadius: CGFloat + if self.isReordering && item.isReorderable { + itemScale = (itemFrame.height - 6.0 * 2.0) / itemFrame.height + itemCornerRadius = 10.0 + } else { + itemScale = 1.0 + itemCornerRadius = 0.0 + } + + let itemAlpha: CGFloat + if let reorderingItem = self.reorderingItem, item.id == reorderingItem.id { + itemAlpha = 0.8 + itemScale = 0.9 + itemFrame = itemFrame.size.centered(around: reorderingItem.position) + } else { + itemAlpha = 1.0 + } + + if transition.animation.isImmediate || isNewlyAdded { + itemLayer.position = itemFrame.center + itemLayer.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) + itemLayer.transform = CATransform3DMakeScale(itemScale, itemScale, 1.0) + itemLayer.layer?.cornerRadius = itemCornerRadius + itemLayer.layer?.opacity = Float(itemAlpha) + if let blurLayer = itemLayer.blurLayer { + blurLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: itemFrame.minY), size: CGSize(width: layout.containerLayout.size.width, height: itemFrame.height)) + } + } else { + if let itemLayerValue = itemLayer.layer { + transition.setPosition(layer: itemLayerValue, position: itemFrame.center) + transition.setBounds(layer: itemLayerValue, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) + transition.setTransform(layer: itemLayerValue, transform: CATransform3DMakeScale(itemScale, itemScale, 1.0)) + transition.setCornerRadius(layer: itemLayerValue, cornerRadius: itemCornerRadius) + transition.setAlpha(layer: itemLayerValue, alpha: itemAlpha) + + if let blurLayer = itemLayer.blurLayer { + blurLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: itemFrame.minY), size: CGSize(width: layout.containerLayout.size.width, height: itemFrame.height)) + } + } else { + itemLayer.position = itemFrame.center + itemLayer.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) + itemLayer.transform = CATransform3DMakeScale(itemScale, itemScale, 1.0) + itemLayer.layer?.cornerRadius = itemCornerRadius + itemLayer.layer?.opacity = Float(itemAlpha) + if let blurLayer = itemLayer.blurLayer { + blurLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: itemFrame.minY), size: CGSize(width: layout.containerLayout.size.width, height: itemFrame.height)) + } + } } } else { let placeholderLayer: SparseItemGridShimmerLayer @@ -1058,6 +1221,22 @@ public final class SparseItemGrid: ASDisplayNode { usedPlaceholderCount += 1 } } + for index in visibleRange.minIndex ... visibleRange.maxIndex { + processItemAtIndex(index) + } + if let reorderingItem = self.reorderingItem, let items = self.items { + var reorderingItemIndex: Int? + for item in items.items { + if item.id == reorderingItem.id { + reorderingItemIndex = item.index + break + } + } + + if let reorderingItemIndex, !(visibleRange.minIndex ... visibleRange.maxIndex).contains(reorderingItemIndex) { + processItemAtIndex(reorderingItemIndex) + } + } } if !bindItems.isEmpty { @@ -1068,9 +1247,20 @@ public final class SparseItemGrid: ASDisplayNode { let item = item as! VisibleItem let contentItem = items.item(at: index) if let layer = item.layer { - layer.update(size: layer.frame.size, insets: layout.containerLayout.insets, displayItem: item, binding: items.itemBinding, item: contentItem) + layer.update(size: layer.bounds.size, insets: layout.containerLayout.insets, displayItem: item, binding: items.itemBinding, item: contentItem) + + if self.isReordering, let contentItem, contentItem.isReorderable { + if layer.animation(forKey: "shaking_position") == nil { + startShaking(layer: layer) + } + } else { + if layer.animation(forKey: "shaking_position") != nil { + layer.removeAnimation(forKey: "shaking_position") + layer.removeAnimation(forKey: "shaking_rotation") + } + } } else if let view = item.view { - view.update(size: view.layer.frame.size, insets: layout.containerLayout.insets) + view.update(size: view.layer.bounds.size, insets: layout.containerLayout.insets) } } @@ -1257,6 +1447,10 @@ public final class SparseItemGrid: ASDisplayNode { return self.fromViewport.coveringInsetOffset * (1.0 - self.currentProgress) + self.toViewport.coveringInsetOffset * self.currentProgress } + var contentBottomOffset: CGFloat { + return self.fromViewport.contentBottomOffset * (1.0 - self.currentProgress) + self.toViewport.contentBottomOffset * self.currentProgress + } + var offset: CGFloat { return self.fromViewport.offset * (1.0 - self.currentProgress) + self.toViewport.offset * self.currentProgress } @@ -1414,10 +1608,14 @@ public final class SparseItemGrid: ASDisplayNode { var lockScrollingAtTop: Bool var fixedItemHeight: CGFloat? var fixedItemAspect: CGFloat? + var adjustForSmallCount: Bool } private var tapRecognizer: UITapGestureRecognizer? private var pinchRecognizer: UIPinchGestureRecognizer? + + private var isReordering: Bool = false + private var reorderRecognizer: ReorderGestureRecognizer? private var theme: PresentationTheme private var containerLayout: ContainerLayout? @@ -1444,6 +1642,16 @@ public final class SparseItemGrid: ASDisplayNode { } } + public var contentBottomOffset: CGFloat { + if let currentViewportTransition = self.currentViewportTransition { + return currentViewportTransition.contentBottomOffset + } else if let currentViewport = self.currentViewport { + return currentViewport.contentBottomOffset + } else { + return 0.0 + } + } + public var scrollingOffset: CGFloat { if let currentViewportTransition = self.currentViewportTransition { return currentViewportTransition.offset @@ -1492,6 +1700,41 @@ public final class SparseItemGrid: ASDisplayNode { let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:))) self.pinchRecognizer = pinchRecognizer self.view.addGestureRecognizer(pinchRecognizer) + + let reorderRecognizer = ReorderGestureRecognizer( + shouldBegin: { [weak self] point in + guard let self, let item = self.item(at: point) else { + return (allowed: false, requiresLongPress: false, item: nil) + } + + return (allowed: true, requiresLongPress: false, item: item) + }, + willBegin: { point in + }, + began: { [weak self] item in + guard let self, let currentViewport = self.currentViewport else { + return + } + currentViewport.setReorderingItem(item: item) + }, + ended: { [weak self] in + guard let self, let currentViewport = self.currentViewport else { + return + } + currentViewport.setReorderingItem(item: nil) + }, + moved: { [weak self] distance in + guard let self, let currentViewport = self.currentViewport else { + return + } + currentViewport.moveReorderingItem(distance: distance) + }, + isActiveUpdated: { _ in + } + ) + self.reorderRecognizer = reorderRecognizer + self.view.addGestureRecognizer(reorderRecognizer) + reorderRecognizer.isEnabled = false self.addSubnode(self.scrollingArea) self.scrollingArea.openCurrentDate = { [weak self] in @@ -1584,7 +1827,7 @@ public final class SparseItemGrid: ASDisplayNode { }) nextViewport.frame = CGRect(origin: CGPoint(), size: containerLayout.size) - nextViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi) + nextViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi, transition: .immediate) self.currentViewportTransition?.removeFromSupernode() @@ -1638,7 +1881,7 @@ public final class SparseItemGrid: ASDisplayNode { }) nextViewport.frame = CGRect(origin: CGPoint(), size: containerLayout.size) - nextViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi) + nextViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi, transition: .immediate) let currentViewportTransition = ViewportTransition(interactiveState: interactiveState, layout: containerLayout, anchorItemIndex: anchorItemIndex, transitionAnchorPoint: anchorLocation, from: previousViewport, to: nextViewport, coveringOffsetUpdated: { [weak self] transition in self?.transitionCoveringOffsetUpdated(transition: transition) @@ -1679,7 +1922,7 @@ public final class SparseItemGrid: ASDisplayNode { strongSelf.scrollingArea.frame = CGRect(origin: CGPoint(), size: containerLayout.size) currentViewport.setScrollingArea(scrollingArea: strongSelf.scrollingArea) currentViewport.frame = CGRect(origin: CGPoint(), size: containerLayout.size) - currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: .semi) + currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: .semi, transition: .immediate) } strongSelf.currentViewportTransition = nil @@ -1691,7 +1934,7 @@ public final class SparseItemGrid: ASDisplayNode { } } - public func update(size: CGSize, insets: UIEdgeInsets, useSideInsets: Bool, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, fixedItemHeight: CGFloat?, fixedItemAspect: CGFloat?, items: Items, theme: PresentationTheme, synchronous: SparseItemGrid.Synchronous) { + public func update(size: CGSize, insets: UIEdgeInsets, useSideInsets: Bool, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, fixedItemHeight: CGFloat?, fixedItemAspect: CGFloat?, adjustForSmallCount: Bool = true, items: Items, theme: PresentationTheme, synchronous: SparseItemGrid.Synchronous, transition: ComponentTransition = .immediate) { self.theme = theme var headerInset: CGFloat = 0.0 @@ -1732,14 +1975,20 @@ public final class SparseItemGrid: ASDisplayNode { var insets = insets insets.top += headerInset - let containerLayout = ContainerLayout(size: size, insets: insets, useSideInsets: useSideInsets, scrollIndicatorInsets: scrollIndicatorInsets, lockScrollingAtTop: lockScrollingAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect) + let containerLayout = ContainerLayout(size: size, insets: insets, useSideInsets: useSideInsets, scrollIndicatorInsets: scrollIndicatorInsets, lockScrollingAtTop: lockScrollingAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, adjustForSmallCount: adjustForSmallCount) self.containerLayout = containerLayout self.items = items self.scrollingArea.isHidden = lockScrollingAtTop - self.tapRecognizer?.isEnabled = fixedItemHeight == nil - self.pinchRecognizer?.isEnabled = fixedItemHeight == nil - + if self.isReordering { + self.tapRecognizer?.isEnabled = false + self.pinchRecognizer?.isEnabled = false + self.reorderRecognizer?.isEnabled = true + } else { + self.tapRecognizer?.isEnabled = fixedItemHeight == nil + self.pinchRecognizer?.isEnabled = fixedItemHeight == nil + self.reorderRecognizer?.isEnabled = false + } if self.currentViewport == nil { let currentViewport = Viewport(theme: self.theme, zoomLevel: self.initialZoomLevel ?? ZoomLevel(rawValue: 3), maybeLoadHoleAnchor: { [weak self] holeAnchor, location in @@ -1762,7 +2011,7 @@ public final class SparseItemGrid: ASDisplayNode { } else if let currentViewport = self.currentViewport { self.scrollingArea.frame = CGRect(origin: CGPoint(), size: size) currentViewport.frame = CGRect(origin: CGPoint(), size: size) - currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: synchronous) + currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: synchronous, transition: transition) } } @@ -1841,7 +2090,7 @@ public final class SparseItemGrid: ASDisplayNode { self.scrollingArea.frame = CGRect(origin: CGPoint(), size: containerLayout.size) currentViewport.frame = CGRect(origin: CGPoint(), size: containerLayout.size) - currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi) + currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi, transition: .immediate) let currentViewportTransition = ViewportTransition(interactiveState: nil, layout: containerLayout, anchorItemIndex: anchorItemIndex, transitionAnchorPoint: anchorLocation, from: previousViewport, to: currentViewport, coveringOffsetUpdated: { [weak self] transition in self?.transitionCoveringOffsetUpdated(transition: transition) @@ -1861,7 +2110,7 @@ public final class SparseItemGrid: ASDisplayNode { strongSelf.insertSubnode(currentViewport, belowSubnode: strongSelf.scrollingArea) strongSelf.scrollingArea.frame = CGRect(origin: CGPoint(), size: containerLayout.size) currentViewport.frame = CGRect(origin: CGPoint(), size: containerLayout.size) - currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: .semi) + currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: .semi, transition: .immediate) } strongSelf.currentViewport?.setScrollingArea(scrollingArea: strongSelf.scrollingArea) @@ -1874,6 +2123,24 @@ public final class SparseItemGrid: ASDisplayNode { } } } + + public func setReordering(isReordering: Bool) { + self.isReordering = isReordering + + if let currentViewport = self.currentViewport { + currentViewport.setReordering(isReordering: isReordering) + } + + if self.isReordering { + self.tapRecognizer?.isEnabled = false + self.pinchRecognizer?.isEnabled = false + self.reorderRecognizer?.isEnabled = true + } else { + self.tapRecognizer?.isEnabled = self.containerLayout?.fixedItemHeight == nil + self.pinchRecognizer?.isEnabled = self.containerLayout?.fixedItemHeight == nil + self.reorderRecognizer?.isEnabled = false + } + } private func coveringOffsetUpdated(viewport: Viewport, transition: ContainedViewLayoutTransition) { guard let items = self.items else { @@ -2050,3 +2317,244 @@ public final class SparseItemGrid: ASDisplayNode { } } } + +private func startShaking(layer: CALayer) { + func degreesToRadians(_ x: CGFloat) -> CGFloat { + return .pi * x / 180.0 + } + + let duration: Double = 0.4 + let displacement: CGFloat = 1.0 + let degreesRotation: CGFloat = 2.0 + + let negativeDisplacement = -1.0 * displacement + let position = CAKeyframeAnimation.init(keyPath: "position") + position.beginTime = 0.8 + position.duration = duration + position.values = [ + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)), + NSValue(cgPoint: CGPoint(x: 0, y: 0)), + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)), + NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)), + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)) + ] + position.calculationMode = .linear + position.isRemovedOnCompletion = false + position.repeatCount = Float.greatestFiniteMagnitude + position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) + position.isAdditive = true + + let transform = CAKeyframeAnimation.init(keyPath: "transform") + transform.beginTime = 2.6 + transform.duration = 0.3 + transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ) + transform.values = [ + degreesToRadians(-1.0 * degreesRotation), + degreesToRadians(degreesRotation), + degreesToRadians(-1.0 * degreesRotation) + ] + transform.calculationMode = .linear + transform.isRemovedOnCompletion = false + transform.repeatCount = Float.greatestFiniteMagnitude + transform.isAdditive = true + transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) + + layer.add(position, forKey: "shaking_position") + layer.add(transform, forKey: "shaking_rotation") +} + +private final class ReorderGestureRecognizer: UIGestureRecognizer { + private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SparseItemGridDisplayItem?) + private let willBegin: (CGPoint) -> Void + private let began: (SparseItemGridDisplayItem) -> Void + private let ended: () -> Void + private let moved: (CGPoint) -> Void + private let isActiveUpdated: (Bool) -> Void + + private var initialLocation: CGPoint? + private var longTapTimer: SwiftSignalKit.Timer? + private var longPressTimer: SwiftSignalKit.Timer? + + private var itemView: SparseItemGridDisplayItem? + + public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SparseItemGridDisplayItem?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (SparseItemGridDisplayItem) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) { + self.shouldBegin = shouldBegin + self.willBegin = willBegin + self.began = began + self.ended = ended + self.moved = moved + self.isActiveUpdated = isActiveUpdated + + super.init(target: nil, action: nil) + } + + deinit { + self.longTapTimer?.invalidate() + self.longPressTimer?.invalidate() + } + + private func startLongTapTimer() { + self.longTapTimer?.invalidate() + let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in + self?.longTapTimerFired() + }, queue: Queue.mainQueue()) + self.longTapTimer = longTapTimer + longTapTimer.start() + } + + private func stopLongTapTimer() { + self.itemView = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + } + + private func startLongPressTimer() { + self.longPressTimer?.invalidate() + let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in + self?.longPressTimerFired() + }, queue: Queue.mainQueue()) + self.longPressTimer = longPressTimer + longPressTimer.start() + } + + private func stopLongPressTimer() { + self.itemView = nil + self.longPressTimer?.invalidate() + self.longPressTimer = nil + } + + override public func reset() { + super.reset() + + self.itemView = nil + self.stopLongTapTimer() + self.stopLongPressTimer() + self.initialLocation = nil + + self.isActiveUpdated(false) + } + + private func longTapTimerFired() { + guard let location = self.initialLocation else { + return + } + + self.longTapTimer?.invalidate() + self.longTapTimer = nil + + self.willBegin(location) + } + + private func longPressTimerFired() { + guard let _ = self.initialLocation else { + return + } + + self.isActiveUpdated(true) + self.state = .began + self.longPressTimer?.invalidate() + self.longPressTimer = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + if let itemView = self.itemView { + self.began(itemView) + } + self.isActiveUpdated(true) + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if self.numberOfTouches > 1 { + self.isActiveUpdated(false) + self.state = .failed + self.ended() + return + } + + if self.state == .possible { + if let location = touches.first?.location(in: self.view) { + let (allowed, requiresLongPress, itemView) = self.shouldBegin(location) + if allowed { + self.isActiveUpdated(true) + + self.itemView = itemView + self.initialLocation = location + if requiresLongPress { + self.startLongTapTimer() + self.startLongPressTimer() + } else { + self.state = .began + if let itemView = self.itemView { + self.began(itemView) + } + } + } else { + self.isActiveUpdated(false) + self.state = .failed + } + } else { + self.isActiveUpdated(false) + self.state = .failed + } + } + } + + override public func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.initialLocation = nil + + self.stopLongTapTimer() + if self.longPressTimer != nil { + self.stopLongPressTimer() + self.isActiveUpdated(false) + self.state = .failed + } + if self.state == .began || self.state == .changed { + self.isActiveUpdated(false) + self.ended() + self.state = .failed + } + } + + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.initialLocation = nil + + self.stopLongTapTimer() + if self.longPressTimer != nil { + self.isActiveUpdated(false) + self.stopLongPressTimer() + self.state = .failed + } + if self.state == .began || self.state == .changed { + self.isActiveUpdated(false) + self.ended() + self.state = .failed + } + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { + self.state = .changed + let offset = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y) + self.moved(offset) + } else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil { + let touchLocation = touch.location(in: self.view) + let dX = touchLocation.x - initialTapLocation.x + let dY = touchLocation.y - initialTapLocation.y + + if dX * dX + dY * dY > 3.0 * 3.0 { + self.stopLongTapTimer() + self.stopLongPressTimer() + self.initialLocation = nil + self.isActiveUpdated(false) + self.state = .failed + } + } + } +} diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index eec5c35f63e..13650ccf002 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -238,8 +238,9 @@ private enum StatsEntry: ItemListNodeEntry { case adsStarsRevenueGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Double) case adsProceedsTitle(PresentationTheme, String) - case adsProceedsOverview(PresentationTheme, RevenueStats, StarsRevenueStats?) - + case adsProceedsOverview(PresentationTheme, RevenueStats?, StarsRevenueStats?) + case adsProceedsInfo(PresentationTheme, String) + case adsTonBalanceTitle(PresentationTheme, String) case adsTonBalance(PresentationTheme, RevenueStats, Bool, Bool) case adsTonBalanceInfo(PresentationTheme, String) @@ -307,7 +308,7 @@ private enum StatsEntry: ItemListNodeEntry { return StatsSection.adsTonRevenue.rawValue case .adsStarsRevenueTitle, .adsStarsRevenueGraph: return StatsSection.adsStarsRevenue.rawValue - case .adsProceedsTitle, .adsProceedsOverview: + case .adsProceedsTitle, .adsProceedsOverview, .adsProceedsInfo: return StatsSection.adsProceeds.rawValue case .adsTonBalanceTitle, .adsTonBalance, .adsTonBalanceInfo: return StatsSection.adsTonBalance.rawValue @@ -430,24 +431,26 @@ private enum StatsEntry: ItemListNodeEntry { return 20007 case .adsProceedsOverview: return 20008 - case .adsTonBalanceTitle: + case .adsProceedsInfo: return 20009 - case .adsTonBalance: + case .adsTonBalanceTitle: return 20010 - case .adsTonBalanceInfo: + case .adsTonBalance: return 20011 - case .adsStarsBalanceTitle: + case .adsTonBalanceInfo: return 20012 - case .adsStarsBalance: + case .adsStarsBalanceTitle: return 20013 - case .adsStarsBalanceInfo: + case .adsStarsBalance: return 20014 - case .adsTransactionsTitle: + case .adsStarsBalanceInfo: return 20015 - case .adsTransactionsTabs: + case .adsTransactionsTitle: return 20016 + case .adsTransactionsTabs: + return 20017 case let .adsTransaction(index, _, _): - return 20017 + index + return 20018 + index case let .adsStarsTransaction(index, _, _): return 30017 + index case .adsTransactionsExpand: @@ -785,6 +788,12 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } + case let .adsProceedsInfo(lhsTheme, lhsText): + if case let .adsProceedsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .adsTonBalanceTitle(lhsTheme, lhsText): if case let .adsTonBalanceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -904,7 +913,8 @@ private enum StatsEntry: ItemListNodeEntry { let .boostersInfo(_, text), let .boostLinkInfo(_, text), let .boostGiftsInfo(_, text), - let .adsCpmInfo(_, text): + let .adsCpmInfo(_, text), + let .adsProceedsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .overview(_, stats): return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats, sectionId: self.section, style: .blocks) @@ -1046,7 +1056,7 @@ private enum StatsEntry: ItemListNodeEntry { arguments.openMonetizationIntro() }) case let .adsProceedsOverview(_, stats, starsStats): - return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats, additionalStats: starsStats, sectionId: self.section, style: .blocks) + return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats ?? starsStats, additionalStats: stats != nil ? starsStats : nil, sectionId: self.section, style: .blocks) case let .adsTonBalance(_, stats, canWithdraw, isEnabled): return MonetizationBalanceItem( context: arguments.context, @@ -1140,7 +1150,7 @@ private enum StatsEntry: ItemListNodeEntry { detailText = stringForMediumCompactDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) } - let label = amountAttributedString(formatBalanceText(transaction.amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + let label = tonAmountAttributedString(formatTonAmountText(transaction.amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor).mutableCopy() as! NSMutableAttributedString label.insert(NSAttributedString(string: " $ ", font: font, textColor: labelColor), at: 1) if let range = label.string.range(of: "$"), let icon = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonMedium"), color: labelColor) { @@ -1516,67 +1526,92 @@ private func monetizationEntries( starsTransactionsInfo: StarsTransactionsContext.State, adsRestricted: Bool, premiumConfiguration: PremiumConfiguration, - monetizationConfiguration: MonetizationConfiguration + monetizationConfiguration: MonetizationConfiguration, + canViewRevenue: Bool, + canViewStarsRevenue: Bool ) -> [StatsEntry] { var entries: [StatsEntry] = [] - entries.append(.adsHeader(presentationData.theme, presentationData.strings.Monetization_Header)) - if !data.topHoursGraph.isEmpty { - entries.append(.adsImpressionsTitle(presentationData.theme, presentationData.strings.Monetization_ImpressionsTitle)) - entries.append(.adsImpressionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.topHoursGraph, .hourlyStep)) + if canViewRevenue { + entries.append(.adsHeader(presentationData.theme, presentationData.strings.Monetization_Header)) + + if !data.topHoursGraph.isEmpty { + entries.append(.adsImpressionsTitle(presentationData.theme, presentationData.strings.Monetization_ImpressionsTitle)) + entries.append(.adsImpressionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.topHoursGraph, .hourlyStep)) + } + + if !data.revenueGraph.isEmpty { + entries.append(.adsTonRevenueTitle(presentationData.theme, presentationData.strings.Monetization_AdRevenueTitle)) + entries.append(.adsTonRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.revenueGraph, .currency, data.usdRate)) + } } - if !data.revenueGraph.isEmpty { - entries.append(.adsTonRevenueTitle(presentationData.theme, presentationData.strings.Monetization_AdRevenueTitle)) - entries.append(.adsTonRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.revenueGraph, .currency, data.usdRate)) + if canViewStarsRevenue { + if let starsData, !starsData.revenueGraph.isEmpty { + entries.append(.adsStarsRevenueTitle(presentationData.theme, presentationData.strings.Monetization_StarsRevenueTitle)) + entries.append(.adsStarsRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, starsData.revenueGraph, .stars, starsData.usdRate)) + } } + + entries.append(.adsProceedsTitle(presentationData.theme, presentationData.strings.Monetization_StarsProceeds_Title)) + entries.append(.adsProceedsOverview(presentationData.theme, canViewRevenue ? data : nil, canViewStarsRevenue ? starsData : nil)) - if let starsData, !starsData.revenueGraph.isEmpty { - entries.append(.adsStarsRevenueTitle(presentationData.theme, presentationData.strings.Monetization_StarsRevenueTitle)) - entries.append(.adsStarsRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, starsData.revenueGraph, .stars, starsData.usdRate)) + let hasTonBalance = data.balances.overallRevenue > 0 + let hasStarsBalance = (starsData?.balances.overallRevenue ?? 0) > 0 + + let proceedsInfo: String + if (canViewStarsRevenue && hasStarsBalance) && (canViewRevenue && hasTonBalance) { + proceedsInfo = presentationData.strings.Monetization_Proceeds_TonAndStars_Info + } else if canViewStarsRevenue && hasStarsBalance { + proceedsInfo = presentationData.strings.Monetization_Proceeds_Stars_Info + } else { + proceedsInfo = presentationData.strings.Monetization_Proceeds_Ton_Info } - - entries.append(.adsProceedsTitle(presentationData.theme, presentationData.strings.Monetization_OverviewTitle)) - entries.append(.adsProceedsOverview(presentationData.theme, data, starsData)) + entries.append(.adsProceedsInfo(presentationData.theme, proceedsInfo)) var isCreator = false if let peer, case let .channel(channel) = peer, channel.flags.contains(.isCreator) { isCreator = true } - entries.append(.adsTonBalanceTitle(presentationData.theme, presentationData.strings.Monetization_TonBalanceTitle)) - entries.append(.adsTonBalance(presentationData.theme, data, isCreator && data.balances.availableBalance > 0, monetizationConfiguration.withdrawalAvailable)) - - if isCreator { - let withdrawalInfoText: String - if data.balances.availableBalance == 0 { - withdrawalInfoText = presentationData.strings.Monetization_Balance_ZeroInfo - } else if monetizationConfiguration.withdrawalAvailable { - withdrawalInfoText = presentationData.strings.Monetization_Balance_AvailableInfo - } else { - withdrawalInfoText = presentationData.strings.Monetization_Balance_ComingLaterInfo + + if canViewRevenue { + entries.append(.adsTonBalanceTitle(presentationData.theme, presentationData.strings.Monetization_TonBalanceTitle)) + entries.append(.adsTonBalance(presentationData.theme, data, isCreator && data.balances.availableBalance > 0, monetizationConfiguration.withdrawalAvailable)) + + if isCreator { + let withdrawalInfoText: String + if data.balances.availableBalance == 0 { + withdrawalInfoText = presentationData.strings.Monetization_Balance_ZeroInfo + } else if monetizationConfiguration.withdrawalAvailable { + withdrawalInfoText = presentationData.strings.Monetization_Balance_AvailableInfo + } else { + withdrawalInfoText = presentationData.strings.Monetization_Balance_ComingLaterInfo + } + entries.append(.adsTonBalanceInfo(presentationData.theme, withdrawalInfoText)) } - entries.append(.adsTonBalanceInfo(presentationData.theme, withdrawalInfoText)) } - if let starsData, starsData.balances.overallRevenue > 0 { - entries.append(.adsStarsBalanceTitle(presentationData.theme, presentationData.strings.Monetization_StarsBalanceTitle)) - entries.append(.adsStarsBalance(presentationData.theme, starsData, isCreator && starsData.balances.availableBalance > 0, starsData.balances.withdrawEnabled, starsData.balances.nextWithdrawalTimestamp)) - entries.append(.adsStarsBalanceInfo(presentationData.theme, presentationData.strings.Monetization_Balance_StarsInfo)) + if canViewStarsRevenue { + if let starsData, starsData.balances.overallRevenue > 0 { + entries.append(.adsStarsBalanceTitle(presentationData.theme, presentationData.strings.Monetization_StarsBalanceTitle)) + entries.append(.adsStarsBalance(presentationData.theme, starsData, isCreator && starsData.balances.availableBalance > 0, starsData.balances.withdrawEnabled, starsData.balances.nextWithdrawalTimestamp)) + entries.append(.adsStarsBalanceInfo(presentationData.theme, presentationData.strings.Monetization_Balance_StarsInfo)) + } } var addedTransactionsTabs = false - if !transactionsInfo.transactions.isEmpty && !starsTransactionsInfo.transactions.isEmpty { + if !transactionsInfo.transactions.isEmpty && !starsTransactionsInfo.transactions.isEmpty && canViewRevenue && canViewStarsRevenue { addedTransactionsTabs = true entries.append(.adsTransactionsTabs(presentationData.theme, presentationData.strings.Monetization_TonTransactions, presentationData.strings.Monetization_StarsTransactions, state.starsSelected)) } var displayTonTransactions = false - if !transactionsInfo.transactions.isEmpty && (starsTransactionsInfo.transactions.isEmpty || !state.starsSelected) { + if canViewRevenue && !transactionsInfo.transactions.isEmpty && (starsTransactionsInfo.transactions.isEmpty || !state.starsSelected) { displayTonTransactions = true } var displayStarsTransactions = false - if !starsTransactionsInfo.transactions.isEmpty && (transactionsInfo.transactions.isEmpty || state.starsSelected) { + if canViewStarsRevenue && !starsTransactionsInfo.transactions.isEmpty && (transactionsInfo.transactions.isEmpty || state.starsSelected) { displayStarsTransactions = true } @@ -1642,7 +1677,7 @@ private func monetizationEntries( } } - if isCreator { + if isCreator && canViewRevenue { var switchOffAdds: Bool? = nil if let boostData, boostData.level >= premiumConfiguration.minChannelRestrictAdsLevel { switchOffAdds = adsRestricted @@ -1675,7 +1710,9 @@ private func channelStatsControllerEntries( starsTransactions: StarsTransactionsContext.State, adsRestricted: Bool, premiumConfiguration: PremiumConfiguration, - monetizationConfiguration: MonetizationConfiguration + monetizationConfiguration: MonetizationConfiguration, + canViewRevenue: Bool, + canViewStarsRevenue: Bool ) -> [StatsEntry] { switch state.section { case .stats: @@ -1715,7 +1752,9 @@ private func channelStatsControllerEntries( starsTransactionsInfo: starsTransactions, adsRestricted: adsRestricted, premiumConfiguration: premiumConfiguration, - monetizationConfiguration: monetizationConfiguration + monetizationConfiguration: monetizationConfiguration, + canViewRevenue: canViewRevenue, + canViewStarsRevenue: canViewStarsRevenue ) } } @@ -2010,9 +2049,12 @@ public func channelStatsController(context: AccountContext, updatedPresentationD let peer = Promise() peer.set(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) + let canViewStatsValue = Atomic(value: true) let peerData = context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.AdsRestricted(id: peerId), - TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId) + TelegramEngine.EngineData.Item.Peer.CanViewStats(id: peerId), + TelegramEngine.EngineData.Item.Peer.AdsRestricted(id: peerId), + TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId), + TelegramEngine.EngineData.Item.Peer.CanViewStarsRevenue(id: peerId) ) let longLoadingSignal: Signal = .single(false) |> then(.single(true) |> delay(2.0, queue: Queue.mainQueue())) @@ -2038,7 +2080,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD ) |> deliverOnMainQueue |> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, starsState, starsTransactions, peerData, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in - let (adsRestricted, canViewRevenue) = peerData + let (canViewStats, adsRestricted, canViewRevenue, canViewStarsRevenue) = peerData + + let _ = canViewStatsValue.swap(canViewStats) var isGroup = false if let peer, case let .channel(channel) = peer, case .group = channel.info { @@ -2116,21 +2160,31 @@ public func channelStatsController(context: AccountContext, updatedPresentationD case .stats: index = 0 case .boosts: - index = 1 + if canViewStats { + index = 1 + } else { + index = 0 + } case .monetization: - index = 2 + if canViewStats { + index = 2 + } else { + index = 1 + } } var tabs: [String] = [] - tabs.append(presentationData.strings.Stats_Statistics) + if canViewStats { + tabs.append(presentationData.strings.Stats_Statistics) + } tabs.append(presentationData.strings.Stats_Boosts) - if canViewRevenue { + if canViewRevenue || canViewStarsRevenue { tabs.append(presentationData.strings.Stats_Monetization) } title = .textWithTabs(peer?.compactDisplayTitle ?? "", tabs, index) } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(presentationData: presentationData, state: state, peer: peer, data: data, messages: messages, stories: stories, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable, isGroup: isGroup, boostsOnly: boostsOnly, revenueState: revenueState?.stats, revenueTransactions: revenueTransactions, starsState: starsState?.stats, starsTransactions: starsTransactions, adsRestricted: adsRestricted, premiumConfiguration: premiumConfiguration, monetizationConfiguration: monetizationConfiguration), style: .blocks, emptyStateItem: emptyStateItem, headerItem: headerItem, crossfadeState: previous == nil, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(presentationData: presentationData, state: state, peer: peer, data: data, messages: messages, stories: stories, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable, isGroup: isGroup, boostsOnly: boostsOnly, revenueState: revenueState?.stats, revenueTransactions: revenueTransactions, starsState: starsState?.stats, starsTransactions: starsTransactions, adsRestricted: adsRestricted, premiumConfiguration: premiumConfiguration, monetizationConfiguration: monetizationConfiguration, canViewRevenue: canViewRevenue, canViewStarsRevenue: canViewStarsRevenue), style: .blocks, emptyStateItem: emptyStateItem, headerItem: headerItem, crossfadeState: previous == nil, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -2152,12 +2206,21 @@ public func channelStatsController(context: AccountContext, updatedPresentationD } controller.titleControlValueChanged = { value in updateState { state in + let canViewStats = canViewStatsValue.with { $0 } let section: ChannelStatsSection switch value { case 0: - section = .stats + if canViewStats { + section = .stats + } else { + section = .boosts + } case 1: - section = .boosts + if canViewStats { + section = .boosts + } else { + section = .monetization + } case 2: section = .monetization let _ = (ApplicationSpecificNotice.monetizationIntroDismissed(accountManager: context.sharedContext.accountManager) @@ -2263,7 +2326,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD } if let navigationController = controller?.navigationController as? NavigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil))) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) } }) }) @@ -2310,7 +2373,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD return } if let navigationController = controller?.navigationController as? NavigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil)) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil)) } }) } diff --git a/submodules/StatisticsUI/Sources/MessageStatsController.swift b/submodules/StatisticsUI/Sources/MessageStatsController.swift index aacc3daa492..3fd238a02d1 100644 --- a/submodules/StatisticsUI/Sources/MessageStatsController.swift +++ b/submodules/StatisticsUI/Sources/MessageStatsController.swift @@ -160,7 +160,7 @@ private enum StatsEntry: ItemListNodeEntry { let .publicForwardsTitle(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .overview(_, stats, storyViews, publicShares): - return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats as! Stats, storyViews: storyViews, publicShares: publicShares, sectionId: self.section, style: .blocks) + return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats as? Stats, storyViews: storyViews, publicShares: publicShares, sectionId: self.section, style: .blocks) case let .interactionsGraph(_, _, _, graph, type, noInitialZoom), let .reactionsGraph(_, _, _, graph, type, noInitialZoom): return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, noInitialZoom: noInitialZoom, getDetailsData: { date, completion in let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in @@ -450,7 +450,7 @@ public func messageStatsController(context: AccountContext, updatedPresentationD return } if let navigationController = controller?.navigationController as? NavigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil)) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil)) } }) } diff --git a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift index b63f1e4c79f..01839afd734 100644 --- a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift +++ b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift @@ -12,6 +12,7 @@ import TextFormat import ComponentFlow import ButtonComponent import BundleIconComponent +import TelegramStringFormatting final class MonetizationBalanceItem: ListViewItem, ItemListItem { let context: AccountContext @@ -175,12 +176,12 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { var isStars = false if let stats = item.stats as? RevenueStats { - let cryptoValue = formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) - amountString = amountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor) - value = stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))" + let cryptoValue = formatTonAmountText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) + amountString = tonAmountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor) + value = stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" } else if let stats = item.stats as? StarsRevenueStats { amountString = NSAttributedString(string: presentationStringsFormattedNumber(Int32(stats.balances.availableBalance), item.presentationData.dateTimeFormat.groupingSeparator), font: integralFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) - value = stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.availableBalance, divide: false, rate: stats.usdRate))" + value = stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" isStars = true } else { fatalError() diff --git a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift index 8e72379fdf8..7df6ea96f50 100644 --- a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift +++ b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift @@ -10,6 +10,7 @@ import PresentationDataUtils import EmojiTextAttachmentView import TextFormat import AccountContext +import TelegramStringFormatting protocol Stats { @@ -47,14 +48,14 @@ class StatsOverviewItem: ListViewItem, ItemListItem { let context: AccountContext let presentationData: ItemListPresentationData let isGroup: Bool - let stats: Stats + let stats: Stats? let additionalStats: Stats? let storyViews: EngineStoryItem.Views? let publicShares: Int32? let sectionId: ItemListSectionId let style: ItemListStyle - init(context: AccountContext, presentationData: ItemListPresentationData, isGroup: Bool, stats: Stats, additionalStats: Stats? = nil, storyViews: EngineStoryItem.Views? = nil, publicShares: Int32? = nil, sectionId: ItemListSectionId, style: ItemListStyle) { + init(context: AccountContext, presentationData: ItemListPresentationData, isGroup: Bool, stats: Stats?, additionalStats: Stats? = nil, storyViews: EngineStoryItem.Views? = nil, publicShares: Int32? = nil, sectionId: ItemListSectionId, style: ItemListStyle) { self.context = context self.presentationData = presentationData self.isGroup = isGroup @@ -200,7 +201,7 @@ private final class ValueItemNode: ASDisplayNode { let valueString: NSAttributedString if case .ton = mode { - valueString = amountAttributedString(value, integralFont: valueFont, fractionalFont: smallValueFont, color: valueColor) + valueString = tonAmountAttributedString(value, integralFont: valueFont, fractionalFont: smallValueFont, color: valueColor) } else { valueString = NSAttributedString(string: value, font: valueFont, textColor: valueColor) } @@ -388,7 +389,7 @@ class StatsOverviewItemNode: ListViewItemNode { } var twoColumnLayout = true - var useMinLeftColumnWidth = true + var useMinLeftColumnWidth = false var topLeftItemLayoutAndApply: (CGSize, () -> ValueItemNode)? var topRightItemLayoutAndApply: (CGSize, () -> ValueItemNode)? @@ -771,9 +772,9 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), item.presentationData.strings.Monetization_StarsProceeds_Available, - (stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))", .generic), + (stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -781,9 +782,9 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatBalanceText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), item.presentationData.strings.Monetization_StarsProceeds_Current, - (stats.balances.currentBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.currentBalance, rate: stats.usdRate))", .generic), + (stats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -791,9 +792,9 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatBalanceText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), item.presentationData.strings.Monetization_StarsProceeds_Total, - (stats.balances.overallRevenue == 0 ? "" : "≈\(formatUsdValue(stats.balances.overallRevenue, rate: stats.usdRate))", .generic), + (stats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -803,7 +804,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, presentationStringsFormattedNumber(Int32(additionalStats.balances.availableBalance), item.presentationData.dateTimeFormat.groupingSeparator), " ", - (additionalStats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(additionalStats.balances.availableBalance, divide: false, rate: additionalStats.usdRate))", .generic), + (additionalStats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(additionalStats.balances.availableBalance, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -813,7 +814,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, presentationStringsFormattedNumber(Int32(additionalStats.balances.currentBalance), item.presentationData.dateTimeFormat.groupingSeparator), " ", - (additionalStats.balances.currentBalance == 0 ? "" : "≈\(formatUsdValue(additionalStats.balances.currentBalance, divide: false, rate: additionalStats.usdRate))", .generic), + (additionalStats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(additionalStats.balances.currentBalance, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -823,7 +824,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, presentationStringsFormattedNumber(Int32(additionalStats.balances.overallRevenue), item.presentationData.dateTimeFormat.groupingSeparator), " ", - (additionalStats.balances.overallRevenue == 0 ? "" : "≈\(formatUsdValue(additionalStats.balances.overallRevenue, divide: false, rate: additionalStats.usdRate))", .generic), + (additionalStats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(additionalStats.balances.overallRevenue, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -835,9 +836,9 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), item.presentationData.strings.Monetization_Overview_Available, - (stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))", .generic), + (stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -845,9 +846,9 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatBalanceText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), item.presentationData.strings.Monetization_Overview_Current, - (stats.balances.currentBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.currentBalance, rate: stats.usdRate))", .generic), + (stats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -855,14 +856,48 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatBalanceText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), item.presentationData.strings.Monetization_Overview_Total, - (stats.balances.overallRevenue == 0 ? "" : "≈\(formatUsdValue(stats.balances.overallRevenue, rate: stats.usdRate))", .generic), + (stats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) height += topLeftItemLayoutAndApply!.0.height * 3.0 + verticalSpacing * 2.0 } + } else if let stats = item.stats as? StarsRevenueStats { + twoColumnLayout = false + + topLeftItemLayoutAndApply = makeTopLeftItemLayout( + item.context, + params.width, + item.presentationData, + presentationStringsFormattedNumber(Int32(stats.balances.availableBalance), item.presentationData.dateTimeFormat.groupingSeparator), + item.presentationData.strings.Monetization_StarsProceeds_Available, + (stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + .stars + ) + + topRightItemLayoutAndApply = makeTopRightItemLayout( + item.context, + params.width, + item.presentationData, + presentationStringsFormattedNumber(Int32(stats.balances.currentBalance), item.presentationData.dateTimeFormat.groupingSeparator), + item.presentationData.strings.Monetization_StarsProceeds_Current, + (stats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + .stars + ) + + middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout( + item.context, + params.width, + item.presentationData, + presentationStringsFormattedNumber(Int32(stats.balances.overallRevenue), item.presentationData.dateTimeFormat.groupingSeparator), + item.presentationData.strings.Monetization_StarsProceeds_Total, + (stats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + .stars + ) + + height += topLeftItemLayoutAndApply!.0.height * 3.0 + verticalSpacing * 2.0 } let contentSize = CGSize(width: params.width, height: height) diff --git a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift index 6479c9b7f2f..a4be4db91a4 100644 --- a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift +++ b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift @@ -139,7 +139,7 @@ private final class SheetContent: CombinedComponent { switch component.transaction { case let .proceeds(amount, fromDate, toDate): labelColor = theme.list.itemDisclosureActions.constructive.fillColor - amountString = amountAttributedString(formatBalanceText(amount, decimalSeparator: dateTimeFormat.decimalSeparator, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + amountString = tonAmountAttributedString(formatTonAmountText(amount, decimalSeparator: dateTimeFormat.decimalSeparator, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString dateString = "\(stringForMediumCompactDate(timestamp: fromDate, strings: strings, dateTimeFormat: dateTimeFormat)) – \(stringForMediumCompactDate(timestamp: toDate, strings: strings, dateTimeFormat: dateTimeFormat))" titleString = strings.Monetization_TransactionInfo_Proceeds buttonTitle = strings.Common_OK @@ -147,7 +147,7 @@ private final class SheetContent: CombinedComponent { showPeer = true case let .withdrawal(status, amount, date, provider, _, transactionUrl): labelColor = theme.list.itemDestructiveColor - amountString = amountAttributedString(formatBalanceText(amount, decimalSeparator: dateTimeFormat.groupingSeparator), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + amountString = tonAmountAttributedString(formatTonAmountText(amount, decimalSeparator: dateTimeFormat.groupingSeparator), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString dateString = stringForFullDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat) switch status { @@ -166,7 +166,7 @@ private final class SheetContent: CombinedComponent { case let .refund(amount, date, _): labelColor = theme.list.itemDisclosureActions.constructive.fillColor titleString = strings.Monetization_TransactionInfo_Refund - amountString = amountAttributedString(formatBalanceText(amount, decimalSeparator: dateTimeFormat.decimalSeparator, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + amountString = tonAmountAttributedString(formatTonAmountText(amount, decimalSeparator: dateTimeFormat.decimalSeparator, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString dateString = stringForFullDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat) buttonTitle = strings.Common_OK explorerUrl = nil diff --git a/submodules/Svg/PublicHeaders/Svg/Svg.h b/submodules/Svg/PublicHeaders/Svg/Svg.h index 51e30030beb..5df838a1622 100755 --- a/submodules/Svg/PublicHeaders/Svg/Svg.h +++ b/submodules/Svg/PublicHeaders/Svg/Svg.h @@ -4,7 +4,7 @@ #import #import -NSData * _Nullable prepareSvgImage(NSData * _Nonnull data); +NSData * _Nullable prepareSvgImage(NSData * _Nonnull data, bool pattern); UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIColor * _Nonnull backgroundColor, CGFloat scale, bool fit); UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor * _Nullable backgroundColor, UIColor * _Nullable foregroundColor, bool opaque); diff --git a/submodules/Svg/Sources/Svg.m b/submodules/Svg/Sources/Svg.m index e86b96599c2..bb242235c55 100755 --- a/submodules/Svg/Sources/Svg.m +++ b/submodules/Svg/Sources/Svg.m @@ -361,14 +361,26 @@ - (void)strokePath { [_data appendBytes:&command length:sizeof(command)]; } +- (void)setFillColor:(uint32_t)color opacity:(CGFloat)opacity { + uint8_t command = 11; + [_data appendBytes:&command length:sizeof(command)]; + + color = ((uint32_t)(opacity * 255.0) << 24) | color; + [_data appendBytes:&color length:sizeof(color)]; +} + @end +UIColor *colorWithBGRA(uint32_t bgra) +{ + return [[UIColor alloc] initWithRed:(((bgra) & 0xff) / 255.0f) green:(((bgra >> 8) & 0xff) / 255.0f) blue:(((bgra >> 16) & 0xff) / 255.0f) alpha:(((bgra >> 24) & 0xff) / 255.0f)]; +} + UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, CGFloat scale, bool fit) { NSDate *startTime = [NSDate date]; UIColor *foregroundColor = [UIColor whiteColor]; - int32_t ptr = 0; int32_t width; int32_t height; @@ -544,7 +556,15 @@ - (void)strokePath { CGContextStrokePath(context); } break; + case 11: + { + uint32_t bgra; + [data getBytes:&bgra range:NSMakeRange(ptr, sizeof(bgra))]; + ptr += sizeof(bgra); + CGContextSetFillColorWithColor(context, colorWithBGRA(bgra).CGColor); + CGContextStrokePath(context); + } default: break; } @@ -559,7 +579,7 @@ - (void)strokePath { return resultImage; } -NSData * _Nullable prepareSvgImage(NSData * _Nonnull data) { +NSData * _Nullable prepareSvgImage(NSData * _Nonnull data, bool template) { NSDate *startTime = [NSDate date]; NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; @@ -600,8 +620,12 @@ - (void)strokePath { } if (shape->fill.type != NSVG_PAINT_NONE) { - [context setFillColorWithOpacity:shape->opacity]; - + if (template) { + [context setFillColorWithOpacity:shape->opacity]; + } else { + [context setFillColor:shape->fill.color opacity:shape->opacity]; + } + bool isFirst = true; bool hasStartPoint = false; CGPoint startPoint; diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 46ce36c45dd..11b0f8515c1 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -105,6 +105,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-944407322] = { return Api.BotMenuButton.parse_botMenuButton($0) } dict[1113113093] = { return Api.BotMenuButton.parse_botMenuButtonCommands($0) } dict[1966318984] = { return Api.BotMenuButton.parse_botMenuButtonDefault($0) } + dict[602479523] = { return Api.BotPreviewMedia.parse_botPreviewMedia($0) } dict[-2076642874] = { return Api.BroadcastRevenueBalances.parse_broadcastRevenueBalances($0) } dict[1434332356] = { return Api.BroadcastRevenueTransaction.parse_broadcastRevenueTransactionProceeds($0) } dict[1121127726] = { return Api.BroadcastRevenueTransaction.parse_broadcastRevenueTransactionRefund($0) } @@ -242,7 +243,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1744710921] = { return Api.DocumentAttribute.parse_documentAttributeHasStickers($0) } dict[1815593308] = { return Api.DocumentAttribute.parse_documentAttributeImageSize($0) } dict[1662637586] = { return Api.DocumentAttribute.parse_documentAttributeSticker($0) } - dict[-745541182] = { return Api.DocumentAttribute.parse_documentAttributeVideo($0) } + dict[389652397] = { return Api.DocumentAttribute.parse_documentAttributeVideo($0) } dict[761606687] = { return Api.DraftMessage.parse_draftMessage($0) } dict[453805082] = { return Api.DraftMessage.parse_draftMessageEmpty($0) } dict[-1764723459] = { return Api.EmailVerification.parse_emailVerificationApple($0) } @@ -354,6 +355,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1690108678] = { return Api.InputEncryptedFile.parse_inputEncryptedFileUploaded($0) } dict[-181407105] = { return Api.InputFile.parse_inputFile($0) } dict[-95482955] = { return Api.InputFile.parse_inputFileBig($0) } + dict[1658620744] = { return Api.InputFile.parse_inputFileStoryDocument($0) } dict[-1160743548] = { return Api.InputFileLocation.parse_inputDocumentFileLocation($0) } dict[-182231723] = { return Api.InputFileLocation.parse_inputEncryptedFileLocation($0) } dict[-539317279] = { return Api.InputFileLocation.parse_inputFileLocation($0) } @@ -373,7 +375,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-977967015] = { return Api.InputInvoice.parse_inputInvoiceMessage($0) } dict[-1734841331] = { return Api.InputInvoice.parse_inputInvoicePremiumGiftCode($0) } dict[-1020867857] = { return Api.InputInvoice.parse_inputInvoiceSlug($0) } - dict[497236696] = { return Api.InputInvoice.parse_inputInvoiceStars($0) } + dict[1710230755] = { return Api.InputInvoice.parse_inputInvoiceStars($0) } dict[-122978821] = { return Api.InputMedia.parse_inputMediaContact($0) } dict[-428884101] = { return Api.InputMedia.parse_inputMediaDice($0) } dict[860303448] = { return Api.InputMedia.parse_inputMediaDocument($0) } @@ -464,7 +466,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1551868097] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) } dict[369444042] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) } dict[-1502273946] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumSubscription($0) } - dict[1326377183] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentStars($0) } + dict[494149367] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentStarsGift($0) } + dict[-572715178] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentStarsTopup($0) } dict[1012306921] = { return Api.InputTheme.parse_inputTheme($0) } dict[-175567375] = { return Api.InputTheme.parse_inputThemeSlug($0) } dict[-1881255857] = { return Api.InputThemeSettings.parse_inputThemeSettings($0) } @@ -519,6 +522,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[340088945] = { return Api.MediaArea.parse_mediaAreaSuggestedReaction($0) } dict[926421125] = { return Api.MediaArea.parse_mediaAreaUrl($0) } dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) } + dict[1235637404] = { return Api.MediaArea.parse_mediaAreaWeather($0) } dict[-808853502] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) } dict[-1808510398] = { return Api.Message.parse_message($0) } dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) } @@ -543,12 +547,14 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1730095465] = { return Api.MessageAction.parse_messageActionGeoProximityReached($0) } dict[1737240073] = { return Api.MessageAction.parse_messageActionGiftCode($0) } dict[-935499028] = { return Api.MessageAction.parse_messageActionGiftPremium($0) } + dict[1171632161] = { return Api.MessageAction.parse_messageActionGiftStars($0) } dict[858499565] = { return Api.MessageAction.parse_messageActionGiveawayLaunch($0) } dict[715107781] = { return Api.MessageAction.parse_messageActionGiveawayResults($0) } dict[2047704898] = { return Api.MessageAction.parse_messageActionGroupCall($0) } dict[-1281329567] = { return Api.MessageAction.parse_messageActionGroupCallScheduled($0) } dict[-1615153660] = { return Api.MessageAction.parse_messageActionHistoryClear($0) } dict[1345295095] = { return Api.MessageAction.parse_messageActionInviteToGroupCall($0) } + dict[1102307842] = { return Api.MessageAction.parse_messageActionPaymentRefunded($0) } dict[-1776926890] = { return Api.MessageAction.parse_messageActionPaymentSent($0) } dict[-1892568281] = { return Api.MessageAction.parse_messageActionPaymentSentMe($0) } dict[-2132731265] = { return Api.MessageAction.parse_messageActionPhoneCall($0) } @@ -873,6 +879,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-425595208] = { return Api.SmsJob.parse_smsJob($0) } dict[-1108478618] = { 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[198776256] = { return Api.StarsTopupOption.parse_starsTopupOption($0) } dict[766853519] = { return Api.StarsTransaction.parse_starsTransaction($0) } @@ -917,6 +924,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($0) } dict[-7173643] = { return Api.Timezone.parse_timezone($0) } dict[-305282981] = { return Api.TopPeer.parse_topPeer($0) } + dict[-39945236] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsApp($0) } dict[344356834] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsInline($0) } dict[-1419371685] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsPM($0) } dict[371037736] = { return Api.TopPeerCategory.parse_topPeerCategoryChannels($0) } @@ -1075,7 +1083,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1886646706] = { return Api.UrlAuthResult.parse_urlAuthResultAccepted($0) } dict[-1445536993] = { return Api.UrlAuthResult.parse_urlAuthResultDefault($0) } dict[-1831650802] = { return Api.UrlAuthResult.parse_urlAuthResultRequest($0) } - dict[559694904] = { return Api.User.parse_user($0) } + dict[-2093920310] = { return Api.User.parse_user($0) } dict[-742634630] = { return Api.User.parse_userEmpty($0) } dict[-862357728] = { return Api.UserFull.parse_userFull($0) } dict[-2100168954] = { return Api.UserProfilePhoto.parse_userProfilePhoto($0) } @@ -1163,6 +1171,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1284008785] = { return Api.auth.SentCodeType.parse_sentCodeTypeSmsPhrase($0) } dict[-1542017919] = { return Api.auth.SentCodeType.parse_sentCodeTypeSmsWord($0) } dict[-391678544] = { return Api.bots.BotInfo.parse_botInfo($0) } + dict[428978491] = { return Api.bots.PopularAppBots.parse_popularAppBots($0) } + dict[212278628] = { return Api.bots.PreviewInfo.parse_previewInfo($0) } dict[-309659827] = { return Api.channels.AdminLogResults.parse_adminLogResults($0) } dict[-541588713] = { return Api.channels.ChannelParticipant.parse_channelParticipant($0) } dict[-1699676497] = { return Api.channels.ChannelParticipants.parse_channelParticipants($0) } @@ -1486,6 +1496,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.BotMenuButton: _1.serialize(buffer, boxed) + case let _1 as Api.BotPreviewMedia: + _1.serialize(buffer, boxed) case let _1 as Api.BroadcastRevenueBalances: _1.serialize(buffer, boxed) case let _1 as Api.BroadcastRevenueTransaction: @@ -1984,6 +1996,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.SponsoredMessageReportOption: _1.serialize(buffer, boxed) + case let _1 as Api.StarsGiftOption: + _1.serialize(buffer, boxed) case let _1 as Api.StarsRevenueStatus: _1.serialize(buffer, boxed) case let _1 as Api.StarsTopupOption: @@ -2140,6 +2154,10 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.bots.BotInfo: _1.serialize(buffer, boxed) + case let _1 as Api.bots.PopularAppBots: + _1.serialize(buffer, boxed) + case let _1 as Api.bots.PreviewInfo: + _1.serialize(buffer, boxed) case let _1 as Api.channels.AdminLogResults: _1.serialize(buffer, boxed) case let _1 as Api.channels.ChannelParticipant: diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index eefc46b7430..79acdb0d0a6 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -1,3 +1,115 @@ +public extension Api { + indirect enum InputInvoice: TypeConstructorDescription { + case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32) + case inputInvoicePremiumGiftCode(purpose: Api.InputStorePaymentPurpose, option: Api.PremiumGiftCodeOption) + case inputInvoiceSlug(slug: String) + case inputInvoiceStars(purpose: Api.InputStorePaymentPurpose) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputInvoiceMessage(let peer, let msgId): + if boxed { + buffer.appendInt32(-977967015) + } + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + break + case .inputInvoicePremiumGiftCode(let purpose, let option): + if boxed { + buffer.appendInt32(-1734841331) + } + purpose.serialize(buffer, true) + option.serialize(buffer, true) + break + case .inputInvoiceSlug(let slug): + if boxed { + buffer.appendInt32(-1020867857) + } + serializeString(slug, buffer: buffer, boxed: false) + break + case .inputInvoiceStars(let purpose): + if boxed { + buffer.appendInt32(1710230755) + } + purpose.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputInvoiceMessage(let peer, let msgId): + return ("inputInvoiceMessage", [("peer", peer as Any), ("msgId", msgId as Any)]) + case .inputInvoicePremiumGiftCode(let purpose, let option): + return ("inputInvoicePremiumGiftCode", [("purpose", purpose as Any), ("option", option as Any)]) + case .inputInvoiceSlug(let slug): + return ("inputInvoiceSlug", [("slug", slug as Any)]) + case .inputInvoiceStars(let purpose): + return ("inputInvoiceStars", [("purpose", purpose as Any)]) + } + } + + public static func parse_inputInvoiceMessage(_ reader: BufferReader) -> InputInvoice? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputInvoice.inputInvoiceMessage(peer: _1!, msgId: _2!) + } + else { + return nil + } + } + public static func parse_inputInvoicePremiumGiftCode(_ reader: BufferReader) -> InputInvoice? { + var _1: Api.InputStorePaymentPurpose? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputStorePaymentPurpose + } + var _2: Api.PremiumGiftCodeOption? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.PremiumGiftCodeOption + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputInvoice.inputInvoicePremiumGiftCode(purpose: _1!, option: _2!) + } + else { + return nil + } + } + public static func parse_inputInvoiceSlug(_ reader: BufferReader) -> InputInvoice? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputInvoice.inputInvoiceSlug(slug: _1!) + } + else { + return nil + } + } + public static func parse_inputInvoiceStars(_ reader: BufferReader) -> InputInvoice? { + var _1: Api.InputStorePaymentPurpose? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputStorePaymentPurpose + } + let _c1 = _1 != nil + if _c1 { + return Api.InputInvoice.inputInvoiceStars(purpose: _1!) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum InputMedia: TypeConstructorDescription { case inputMediaContact(phoneNumber: String, firstName: String, lastName: String, vcard: String) @@ -918,171 +1030,3 @@ public extension Api { } } -public extension Api { - indirect enum InputPeer: TypeConstructorDescription { - case inputPeerChannel(channelId: Int64, accessHash: Int64) - case inputPeerChannelFromMessage(peer: Api.InputPeer, msgId: Int32, channelId: Int64) - case inputPeerChat(chatId: Int64) - case inputPeerEmpty - case inputPeerSelf - case inputPeerUser(userId: Int64, accessHash: Int64) - case inputPeerUserFromMessage(peer: Api.InputPeer, msgId: Int32, userId: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputPeerChannel(let channelId, let accessHash): - if boxed { - buffer.appendInt32(666680316) - } - serializeInt64(channelId, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputPeerChannelFromMessage(let peer, let msgId, let channelId): - if boxed { - buffer.appendInt32(-1121318848) - } - peer.serialize(buffer, true) - serializeInt32(msgId, buffer: buffer, boxed: false) - serializeInt64(channelId, buffer: buffer, boxed: false) - break - case .inputPeerChat(let chatId): - if boxed { - buffer.appendInt32(900291769) - } - serializeInt64(chatId, buffer: buffer, boxed: false) - break - case .inputPeerEmpty: - if boxed { - buffer.appendInt32(2134579434) - } - - break - case .inputPeerSelf: - if boxed { - buffer.appendInt32(2107670217) - } - - break - case .inputPeerUser(let userId, let accessHash): - if boxed { - buffer.appendInt32(-571955892) - } - serializeInt64(userId, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputPeerUserFromMessage(let peer, let msgId, let userId): - if boxed { - buffer.appendInt32(-1468331492) - } - peer.serialize(buffer, true) - serializeInt32(msgId, buffer: buffer, boxed: false) - serializeInt64(userId, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputPeerChannel(let channelId, let accessHash): - return ("inputPeerChannel", [("channelId", channelId as Any), ("accessHash", accessHash as Any)]) - case .inputPeerChannelFromMessage(let peer, let msgId, let channelId): - return ("inputPeerChannelFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("channelId", channelId as Any)]) - case .inputPeerChat(let chatId): - return ("inputPeerChat", [("chatId", chatId as Any)]) - case .inputPeerEmpty: - return ("inputPeerEmpty", []) - case .inputPeerSelf: - return ("inputPeerSelf", []) - case .inputPeerUser(let userId, let accessHash): - return ("inputPeerUser", [("userId", userId as Any), ("accessHash", accessHash as Any)]) - case .inputPeerUserFromMessage(let peer, let msgId, let userId): - return ("inputPeerUserFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("userId", userId as Any)]) - } - } - - public static func parse_inputPeerChannel(_ reader: BufferReader) -> InputPeer? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputPeer.inputPeerChannel(channelId: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputPeerChannelFromMessage(_ reader: BufferReader) -> InputPeer? { - var _1: Api.InputPeer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputPeer - } - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputPeer.inputPeerChannelFromMessage(peer: _1!, msgId: _2!, channelId: _3!) - } - else { - return nil - } - } - public static func parse_inputPeerChat(_ reader: BufferReader) -> InputPeer? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return Api.InputPeer.inputPeerChat(chatId: _1!) - } - else { - return nil - } - } - public static func parse_inputPeerEmpty(_ reader: BufferReader) -> InputPeer? { - return Api.InputPeer.inputPeerEmpty - } - public static func parse_inputPeerSelf(_ reader: BufferReader) -> InputPeer? { - return Api.InputPeer.inputPeerSelf - } - public static func parse_inputPeerUser(_ reader: BufferReader) -> InputPeer? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputPeer.inputPeerUser(userId: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputPeerUserFromMessage(_ reader: BufferReader) -> InputPeer? { - var _1: Api.InputPeer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputPeer - } - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputPeer.inputPeerUserFromMessage(peer: _1!, msgId: _2!, userId: _3!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index 8a502fcddf3..8845ce3ec63 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -1,3 +1,171 @@ +public extension Api { + indirect enum InputPeer: TypeConstructorDescription { + case inputPeerChannel(channelId: Int64, accessHash: Int64) + case inputPeerChannelFromMessage(peer: Api.InputPeer, msgId: Int32, channelId: Int64) + case inputPeerChat(chatId: Int64) + case inputPeerEmpty + case inputPeerSelf + case inputPeerUser(userId: Int64, accessHash: Int64) + case inputPeerUserFromMessage(peer: Api.InputPeer, msgId: Int32, userId: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputPeerChannel(let channelId, let accessHash): + if boxed { + buffer.appendInt32(666680316) + } + serializeInt64(channelId, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputPeerChannelFromMessage(let peer, let msgId, let channelId): + if boxed { + buffer.appendInt32(-1121318848) + } + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + serializeInt64(channelId, buffer: buffer, boxed: false) + break + case .inputPeerChat(let chatId): + if boxed { + buffer.appendInt32(900291769) + } + serializeInt64(chatId, buffer: buffer, boxed: false) + break + case .inputPeerEmpty: + if boxed { + buffer.appendInt32(2134579434) + } + + break + case .inputPeerSelf: + if boxed { + buffer.appendInt32(2107670217) + } + + break + case .inputPeerUser(let userId, let accessHash): + if boxed { + buffer.appendInt32(-571955892) + } + serializeInt64(userId, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputPeerUserFromMessage(let peer, let msgId, let userId): + if boxed { + buffer.appendInt32(-1468331492) + } + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + serializeInt64(userId, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputPeerChannel(let channelId, let accessHash): + return ("inputPeerChannel", [("channelId", channelId as Any), ("accessHash", accessHash as Any)]) + case .inputPeerChannelFromMessage(let peer, let msgId, let channelId): + return ("inputPeerChannelFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("channelId", channelId as Any)]) + case .inputPeerChat(let chatId): + return ("inputPeerChat", [("chatId", chatId as Any)]) + case .inputPeerEmpty: + return ("inputPeerEmpty", []) + case .inputPeerSelf: + return ("inputPeerSelf", []) + case .inputPeerUser(let userId, let accessHash): + return ("inputPeerUser", [("userId", userId as Any), ("accessHash", accessHash as Any)]) + case .inputPeerUserFromMessage(let peer, let msgId, let userId): + return ("inputPeerUserFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("userId", userId as Any)]) + } + } + + public static func parse_inputPeerChannel(_ reader: BufferReader) -> InputPeer? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputPeer.inputPeerChannel(channelId: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputPeerChannelFromMessage(_ reader: BufferReader) -> InputPeer? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputPeer.inputPeerChannelFromMessage(peer: _1!, msgId: _2!, channelId: _3!) + } + else { + return nil + } + } + public static func parse_inputPeerChat(_ reader: BufferReader) -> InputPeer? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.InputPeer.inputPeerChat(chatId: _1!) + } + else { + return nil + } + } + public static func parse_inputPeerEmpty(_ reader: BufferReader) -> InputPeer? { + return Api.InputPeer.inputPeerEmpty + } + public static func parse_inputPeerSelf(_ reader: BufferReader) -> InputPeer? { + return Api.InputPeer.inputPeerSelf + } + public static func parse_inputPeerUser(_ reader: BufferReader) -> InputPeer? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputPeer.inputPeerUser(userId: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputPeerUserFromMessage(_ reader: BufferReader) -> InputPeer? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputPeer.inputPeerUserFromMessage(peer: _1!, msgId: _2!, userId: _3!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputPeerNotifySettings: TypeConstructorDescription { case inputPeerNotifySettings(flags: Int32, showPreviews: Api.Bool?, silent: Api.Bool?, muteUntil: Int32?, sound: Api.NotificationSound?, storiesMuted: Api.Bool?, storiesHideSender: Api.Bool?, storiesSound: Api.NotificationSound?) @@ -510,321 +678,3 @@ public extension Api { } } -public extension Api { - enum InputQuickReplyShortcut: TypeConstructorDescription { - case inputQuickReplyShortcut(shortcut: String) - case inputQuickReplyShortcutId(shortcutId: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputQuickReplyShortcut(let shortcut): - if boxed { - buffer.appendInt32(609840449) - } - serializeString(shortcut, buffer: buffer, boxed: false) - break - case .inputQuickReplyShortcutId(let shortcutId): - if boxed { - buffer.appendInt32(18418929) - } - serializeInt32(shortcutId, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputQuickReplyShortcut(let shortcut): - return ("inputQuickReplyShortcut", [("shortcut", shortcut as Any)]) - case .inputQuickReplyShortcutId(let shortcutId): - return ("inputQuickReplyShortcutId", [("shortcutId", shortcutId as Any)]) - } - } - - public static func parse_inputQuickReplyShortcut(_ reader: BufferReader) -> InputQuickReplyShortcut? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.InputQuickReplyShortcut.inputQuickReplyShortcut(shortcut: _1!) - } - else { - return nil - } - } - public static func parse_inputQuickReplyShortcutId(_ reader: BufferReader) -> InputQuickReplyShortcut? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.InputQuickReplyShortcut.inputQuickReplyShortcutId(shortcutId: _1!) - } - else { - return nil - } - } - - } -} -public extension Api { - indirect enum InputReplyTo: TypeConstructorDescription { - case inputReplyToMessage(flags: Int32, replyToMsgId: Int32, topMsgId: Int32?, replyToPeerId: Api.InputPeer?, quoteText: String?, quoteEntities: [Api.MessageEntity]?, quoteOffset: Int32?) - case inputReplyToStory(peer: Api.InputPeer, storyId: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId, let replyToPeerId, let quoteText, let quoteEntities, let quoteOffset): - if boxed { - buffer.appendInt32(583071445) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(replyToMsgId, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {replyToPeerId!.serialize(buffer, true)} - if Int(flags) & Int(1 << 2) != 0 {serializeString(quoteText!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(quoteEntities!.count)) - for item in quoteEntities! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 4) != 0 {serializeInt32(quoteOffset!, buffer: buffer, boxed: false)} - break - case .inputReplyToStory(let peer, let storyId): - if boxed { - buffer.appendInt32(1484862010) - } - peer.serialize(buffer, true) - serializeInt32(storyId, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId, let replyToPeerId, let quoteText, let quoteEntities, let quoteOffset): - return ("inputReplyToMessage", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("topMsgId", topMsgId as Any), ("replyToPeerId", replyToPeerId as Any), ("quoteText", quoteText as Any), ("quoteEntities", quoteEntities as Any), ("quoteOffset", quoteOffset as Any)]) - case .inputReplyToStory(let peer, let storyId): - return ("inputReplyToStory", [("peer", peer as Any), ("storyId", storyId as Any)]) - } - } - - public static func parse_inputReplyToMessage(_ reader: BufferReader) -> InputReplyTo? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() } - var _4: Api.InputPeer? - if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.InputPeer - } } - var _5: String? - if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) } - var _6: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) - } } - var _7: Int32? - if Int(_1!) & Int(1 << 4) != 0 {_7 = reader.readInt32() } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.InputReplyTo.inputReplyToMessage(flags: _1!, replyToMsgId: _2!, topMsgId: _3, replyToPeerId: _4, quoteText: _5, quoteEntities: _6, quoteOffset: _7) - } - else { - return nil - } - } - public static func parse_inputReplyToStory(_ reader: BufferReader) -> InputReplyTo? { - var _1: Api.InputPeer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputPeer - } - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputReplyTo.inputReplyToStory(peer: _1!, storyId: _2!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum InputSecureFile: TypeConstructorDescription { - case inputSecureFile(id: Int64, accessHash: Int64) - case inputSecureFileUploaded(id: Int64, parts: Int32, md5Checksum: String, fileHash: Buffer, secret: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputSecureFile(let id, let accessHash): - if boxed { - buffer.appendInt32(1399317950) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputSecureFileUploaded(let id, let parts, let md5Checksum, let fileHash, let secret): - if boxed { - buffer.appendInt32(859091184) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt32(parts, buffer: buffer, boxed: false) - serializeString(md5Checksum, buffer: buffer, boxed: false) - serializeBytes(fileHash, buffer: buffer, boxed: false) - serializeBytes(secret, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputSecureFile(let id, let accessHash): - return ("inputSecureFile", [("id", id as Any), ("accessHash", accessHash as Any)]) - case .inputSecureFileUploaded(let id, let parts, let md5Checksum, let fileHash, let secret): - return ("inputSecureFileUploaded", [("id", id as Any), ("parts", parts as Any), ("md5Checksum", md5Checksum as Any), ("fileHash", fileHash as Any), ("secret", secret as Any)]) - } - } - - public static func parse_inputSecureFile(_ reader: BufferReader) -> InputSecureFile? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputSecureFile.inputSecureFile(id: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputSecureFileUploaded(_ reader: BufferReader) -> InputSecureFile? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - var _4: Buffer? - _4 = parseBytes(reader) - var _5: Buffer? - _5 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.InputSecureFile.inputSecureFileUploaded(id: _1!, parts: _2!, md5Checksum: _3!, fileHash: _4!, secret: _5!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum InputSecureValue: TypeConstructorDescription { - case inputSecureValue(flags: Int32, type: Api.SecureValueType, data: Api.SecureData?, frontSide: Api.InputSecureFile?, reverseSide: Api.InputSecureFile?, selfie: Api.InputSecureFile?, translation: [Api.InputSecureFile]?, files: [Api.InputSecureFile]?, plainData: Api.SecurePlainData?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputSecureValue(let flags, let type, let data, let frontSide, let reverseSide, let selfie, let translation, let files, let plainData): - if boxed { - buffer.appendInt32(-618540889) - } - serializeInt32(flags, buffer: buffer, boxed: false) - type.serialize(buffer, true) - if Int(flags) & Int(1 << 0) != 0 {data!.serialize(buffer, true)} - if Int(flags) & Int(1 << 1) != 0 {frontSide!.serialize(buffer, true)} - if Int(flags) & Int(1 << 2) != 0 {reverseSide!.serialize(buffer, true)} - if Int(flags) & Int(1 << 3) != 0 {selfie!.serialize(buffer, true)} - if Int(flags) & Int(1 << 6) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(translation!.count)) - for item in translation! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(files!.count)) - for item in files! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 5) != 0 {plainData!.serialize(buffer, true)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputSecureValue(let flags, let type, let data, let frontSide, let reverseSide, let selfie, let translation, let files, let plainData): - return ("inputSecureValue", [("flags", flags as Any), ("type", type as Any), ("data", data as Any), ("frontSide", frontSide as Any), ("reverseSide", reverseSide as Any), ("selfie", selfie as Any), ("translation", translation as Any), ("files", files as Any), ("plainData", plainData as Any)]) - } - } - - public static func parse_inputSecureValue(_ reader: BufferReader) -> InputSecureValue? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.SecureValueType? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.SecureValueType - } - var _3: Api.SecureData? - if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.SecureData - } } - var _4: Api.InputSecureFile? - if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.InputSecureFile - } } - var _5: Api.InputSecureFile? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.InputSecureFile - } } - var _6: Api.InputSecureFile? - if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.InputSecureFile - } } - var _7: [Api.InputSecureFile]? - if Int(_1!) & Int(1 << 6) != 0 {if let _ = reader.readInt32() { - _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputSecureFile.self) - } } - var _8: [Api.InputSecureFile]? - if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() { - _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputSecureFile.self) - } } - var _9: Api.SecurePlainData? - if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() { - _9 = Api.parse(reader, signature: signature) as? Api.SecurePlainData - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 6) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 5) == 0) || _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.InputSecureValue.inputSecureValue(flags: _1!, type: _2!, data: _3, frontSide: _4, reverseSide: _5, selfie: _6, translation: _7, files: _8, plainData: _9) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index 66a1ef38225..ac16a872404 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -1,3 +1,321 @@ +public extension Api { + enum InputQuickReplyShortcut: TypeConstructorDescription { + case inputQuickReplyShortcut(shortcut: String) + case inputQuickReplyShortcutId(shortcutId: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputQuickReplyShortcut(let shortcut): + if boxed { + buffer.appendInt32(609840449) + } + serializeString(shortcut, buffer: buffer, boxed: false) + break + case .inputQuickReplyShortcutId(let shortcutId): + if boxed { + buffer.appendInt32(18418929) + } + serializeInt32(shortcutId, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputQuickReplyShortcut(let shortcut): + return ("inputQuickReplyShortcut", [("shortcut", shortcut as Any)]) + case .inputQuickReplyShortcutId(let shortcutId): + return ("inputQuickReplyShortcutId", [("shortcutId", shortcutId as Any)]) + } + } + + public static func parse_inputQuickReplyShortcut(_ reader: BufferReader) -> InputQuickReplyShortcut? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputQuickReplyShortcut.inputQuickReplyShortcut(shortcut: _1!) + } + else { + return nil + } + } + public static func parse_inputQuickReplyShortcutId(_ reader: BufferReader) -> InputQuickReplyShortcut? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.InputQuickReplyShortcut.inputQuickReplyShortcutId(shortcutId: _1!) + } + else { + return nil + } + } + + } +} +public extension Api { + indirect enum InputReplyTo: TypeConstructorDescription { + case inputReplyToMessage(flags: Int32, replyToMsgId: Int32, topMsgId: Int32?, replyToPeerId: Api.InputPeer?, quoteText: String?, quoteEntities: [Api.MessageEntity]?, quoteOffset: Int32?) + case inputReplyToStory(peer: Api.InputPeer, storyId: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId, let replyToPeerId, let quoteText, let quoteEntities, let quoteOffset): + if boxed { + buffer.appendInt32(583071445) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(replyToMsgId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {replyToPeerId!.serialize(buffer, true)} + if Int(flags) & Int(1 << 2) != 0 {serializeString(quoteText!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(quoteEntities!.count)) + for item in quoteEntities! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 4) != 0 {serializeInt32(quoteOffset!, buffer: buffer, boxed: false)} + break + case .inputReplyToStory(let peer, let storyId): + if boxed { + buffer.appendInt32(1484862010) + } + peer.serialize(buffer, true) + serializeInt32(storyId, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId, let replyToPeerId, let quoteText, let quoteEntities, let quoteOffset): + return ("inputReplyToMessage", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("topMsgId", topMsgId as Any), ("replyToPeerId", replyToPeerId as Any), ("quoteText", quoteText as Any), ("quoteEntities", quoteEntities as Any), ("quoteOffset", quoteOffset as Any)]) + case .inputReplyToStory(let peer, let storyId): + return ("inputReplyToStory", [("peer", peer as Any), ("storyId", storyId as Any)]) + } + } + + public static func parse_inputReplyToMessage(_ reader: BufferReader) -> InputReplyTo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() } + var _4: Api.InputPeer? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.InputPeer + } } + var _5: String? + if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) } + var _6: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } } + var _7: Int32? + if Int(_1!) & Int(1 << 4) != 0 {_7 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.InputReplyTo.inputReplyToMessage(flags: _1!, replyToMsgId: _2!, topMsgId: _3, replyToPeerId: _4, quoteText: _5, quoteEntities: _6, quoteOffset: _7) + } + else { + return nil + } + } + public static func parse_inputReplyToStory(_ reader: BufferReader) -> InputReplyTo? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputReplyTo.inputReplyToStory(peer: _1!, storyId: _2!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum InputSecureFile: TypeConstructorDescription { + case inputSecureFile(id: Int64, accessHash: Int64) + case inputSecureFileUploaded(id: Int64, parts: Int32, md5Checksum: String, fileHash: Buffer, secret: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputSecureFile(let id, let accessHash): + if boxed { + buffer.appendInt32(1399317950) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputSecureFileUploaded(let id, let parts, let md5Checksum, let fileHash, let secret): + if boxed { + buffer.appendInt32(859091184) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt32(parts, buffer: buffer, boxed: false) + serializeString(md5Checksum, buffer: buffer, boxed: false) + serializeBytes(fileHash, buffer: buffer, boxed: false) + serializeBytes(secret, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputSecureFile(let id, let accessHash): + return ("inputSecureFile", [("id", id as Any), ("accessHash", accessHash as Any)]) + case .inputSecureFileUploaded(let id, let parts, let md5Checksum, let fileHash, let secret): + return ("inputSecureFileUploaded", [("id", id as Any), ("parts", parts as Any), ("md5Checksum", md5Checksum as Any), ("fileHash", fileHash as Any), ("secret", secret as Any)]) + } + } + + public static func parse_inputSecureFile(_ reader: BufferReader) -> InputSecureFile? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputSecureFile.inputSecureFile(id: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputSecureFileUploaded(_ reader: BufferReader) -> InputSecureFile? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + var _4: Buffer? + _4 = parseBytes(reader) + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.InputSecureFile.inputSecureFileUploaded(id: _1!, parts: _2!, md5Checksum: _3!, fileHash: _4!, secret: _5!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum InputSecureValue: TypeConstructorDescription { + case inputSecureValue(flags: Int32, type: Api.SecureValueType, data: Api.SecureData?, frontSide: Api.InputSecureFile?, reverseSide: Api.InputSecureFile?, selfie: Api.InputSecureFile?, translation: [Api.InputSecureFile]?, files: [Api.InputSecureFile]?, plainData: Api.SecurePlainData?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputSecureValue(let flags, let type, let data, let frontSide, let reverseSide, let selfie, let translation, let files, let plainData): + if boxed { + buffer.appendInt32(-618540889) + } + serializeInt32(flags, buffer: buffer, boxed: false) + type.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {data!.serialize(buffer, true)} + if Int(flags) & Int(1 << 1) != 0 {frontSide!.serialize(buffer, true)} + if Int(flags) & Int(1 << 2) != 0 {reverseSide!.serialize(buffer, true)} + if Int(flags) & Int(1 << 3) != 0 {selfie!.serialize(buffer, true)} + if Int(flags) & Int(1 << 6) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(translation!.count)) + for item in translation! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(files!.count)) + for item in files! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 5) != 0 {plainData!.serialize(buffer, true)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputSecureValue(let flags, let type, let data, let frontSide, let reverseSide, let selfie, let translation, let files, let plainData): + return ("inputSecureValue", [("flags", flags as Any), ("type", type as Any), ("data", data as Any), ("frontSide", frontSide as Any), ("reverseSide", reverseSide as Any), ("selfie", selfie as Any), ("translation", translation as Any), ("files", files as Any), ("plainData", plainData as Any)]) + } + } + + public static func parse_inputSecureValue(_ reader: BufferReader) -> InputSecureValue? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.SecureValueType? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.SecureValueType + } + var _3: Api.SecureData? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.SecureData + } } + var _4: Api.InputSecureFile? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.InputSecureFile + } } + var _5: Api.InputSecureFile? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.InputSecureFile + } } + var _6: Api.InputSecureFile? + if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.InputSecureFile + } } + var _7: [Api.InputSecureFile]? + if Int(_1!) & Int(1 << 6) != 0 {if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputSecureFile.self) + } } + var _8: [Api.InputSecureFile]? + if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() { + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputSecureFile.self) + } } + var _9: Api.SecurePlainData? + if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.SecurePlainData + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 6) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 5) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.InputSecureValue.inputSecureValue(flags: _1!, type: _2!, data: _3, frontSide: _4, reverseSide: _5, selfie: _6, translation: _7, files: _8, plainData: _9) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum InputSingleMedia: TypeConstructorDescription { case inputSingleMedia(flags: Int32, media: Api.InputMedia, randomId: Int64, message: String, entities: [Api.MessageEntity]?) @@ -396,7 +714,8 @@ public extension Api { case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64) case inputStorePaymentPremiumGiveaway(flags: Int32, boostPeer: Api.InputPeer, additionalPeers: [Api.InputPeer]?, countriesIso2: [String]?, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64) case inputStorePaymentPremiumSubscription(flags: Int32) - case inputStorePaymentStars(flags: Int32, stars: Int64, currency: String, amount: Int64) + case inputStorePaymentStarsGift(userId: Api.InputUser, stars: Int64, currency: String, amount: Int64) + case inputStorePaymentStarsTopup(stars: Int64, currency: String, amount: Int64) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -450,11 +769,19 @@ public extension Api { } serializeInt32(flags, buffer: buffer, boxed: false) break - case .inputStorePaymentStars(let flags, let stars, let currency, let amount): + case .inputStorePaymentStarsGift(let userId, let stars, let currency, let amount): if boxed { - buffer.appendInt32(1326377183) + buffer.appendInt32(494149367) + } + userId.serialize(buffer, true) + serializeInt64(stars, buffer: buffer, boxed: false) + serializeString(currency, buffer: buffer, boxed: false) + serializeInt64(amount, buffer: buffer, boxed: false) + break + case .inputStorePaymentStarsTopup(let stars, let currency, let amount): + if boxed { + buffer.appendInt32(-572715178) } - serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(stars, buffer: buffer, boxed: false) serializeString(currency, buffer: buffer, boxed: false) serializeInt64(amount, buffer: buffer, boxed: false) @@ -472,8 +799,10 @@ public extension Api { return ("inputStorePaymentPremiumGiveaway", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("additionalPeers", additionalPeers as Any), ("countriesIso2", countriesIso2 as Any), ("prizeDescription", prizeDescription as Any), ("randomId", randomId as Any), ("untilDate", untilDate as Any), ("currency", currency as Any), ("amount", amount as Any)]) case .inputStorePaymentPremiumSubscription(let flags): return ("inputStorePaymentPremiumSubscription", [("flags", flags as Any)]) - case .inputStorePaymentStars(let flags, let stars, let currency, let amount): - return ("inputStorePaymentStars", [("flags", flags as Any), ("stars", stars as Any), ("currency", currency as Any), ("amount", amount as Any)]) + case .inputStorePaymentStarsGift(let userId, let stars, let currency, let amount): + return ("inputStorePaymentStarsGift", [("userId", userId as Any), ("stars", stars as Any), ("currency", currency as Any), ("amount", amount as Any)]) + case .inputStorePaymentStarsTopup(let stars, let currency, let amount): + return ("inputStorePaymentStarsTopup", [("stars", stars as Any), ("currency", currency as Any), ("amount", amount as Any)]) } } @@ -575,9 +904,11 @@ public extension Api { return nil } } - public static func parse_inputStorePaymentStars(_ reader: BufferReader) -> InputStorePaymentPurpose? { - var _1: Int32? - _1 = reader.readInt32() + public static func parse_inputStorePaymentStarsGift(_ reader: BufferReader) -> InputStorePaymentPurpose? { + var _1: Api.InputUser? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputUser + } var _2: Int64? _2 = reader.readInt64() var _3: String? @@ -589,7 +920,24 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { - return Api.InputStorePaymentPurpose.inputStorePaymentStars(flags: _1!, stars: _2!, currency: _3!, amount: _4!) + return Api.InputStorePaymentPurpose.inputStorePaymentStarsGift(userId: _1!, stars: _2!, currency: _3!, amount: _4!) + } + else { + return nil + } + } + public static func parse_inputStorePaymentStarsTopup(_ reader: BufferReader) -> InputStorePaymentPurpose? { + var _1: Int64? + _1 = reader.readInt64() + var _2: String? + _2 = parseString(reader) + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputStorePaymentPurpose.inputStorePaymentStarsTopup(stars: _1!, currency: _2!, amount: _3!) } else { return nil @@ -730,231 +1078,3 @@ public extension Api { } } -public extension Api { - indirect enum InputUser: TypeConstructorDescription { - case inputUser(userId: Int64, accessHash: Int64) - case inputUserEmpty - case inputUserFromMessage(peer: Api.InputPeer, msgId: Int32, userId: Int64) - case inputUserSelf - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputUser(let userId, let accessHash): - if boxed { - buffer.appendInt32(-233744186) - } - serializeInt64(userId, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputUserEmpty: - if boxed { - buffer.appendInt32(-1182234929) - } - - break - case .inputUserFromMessage(let peer, let msgId, let userId): - if boxed { - buffer.appendInt32(497305826) - } - peer.serialize(buffer, true) - serializeInt32(msgId, buffer: buffer, boxed: false) - serializeInt64(userId, buffer: buffer, boxed: false) - break - case .inputUserSelf: - if boxed { - buffer.appendInt32(-138301121) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputUser(let userId, let accessHash): - return ("inputUser", [("userId", userId as Any), ("accessHash", accessHash as Any)]) - case .inputUserEmpty: - return ("inputUserEmpty", []) - case .inputUserFromMessage(let peer, let msgId, let userId): - return ("inputUserFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("userId", userId as Any)]) - case .inputUserSelf: - return ("inputUserSelf", []) - } - } - - public static func parse_inputUser(_ reader: BufferReader) -> InputUser? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputUser.inputUser(userId: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputUserEmpty(_ reader: BufferReader) -> InputUser? { - return Api.InputUser.inputUserEmpty - } - public static func parse_inputUserFromMessage(_ reader: BufferReader) -> InputUser? { - var _1: Api.InputPeer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputPeer - } - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputUser.inputUserFromMessage(peer: _1!, msgId: _2!, userId: _3!) - } - else { - return nil - } - } - public static func parse_inputUserSelf(_ reader: BufferReader) -> InputUser? { - return Api.InputUser.inputUserSelf - } - - } -} -public extension Api { - enum InputWallPaper: TypeConstructorDescription { - case inputWallPaper(id: Int64, accessHash: Int64) - case inputWallPaperNoFile(id: Int64) - case inputWallPaperSlug(slug: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputWallPaper(let id, let accessHash): - if boxed { - buffer.appendInt32(-433014407) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputWallPaperNoFile(let id): - if boxed { - buffer.appendInt32(-1770371538) - } - serializeInt64(id, buffer: buffer, boxed: false) - break - case .inputWallPaperSlug(let slug): - if boxed { - buffer.appendInt32(1913199744) - } - serializeString(slug, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputWallPaper(let id, let accessHash): - return ("inputWallPaper", [("id", id as Any), ("accessHash", accessHash as Any)]) - case .inputWallPaperNoFile(let id): - return ("inputWallPaperNoFile", [("id", id as Any)]) - case .inputWallPaperSlug(let slug): - return ("inputWallPaperSlug", [("slug", slug as Any)]) - } - } - - public static func parse_inputWallPaper(_ reader: BufferReader) -> InputWallPaper? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputWallPaper.inputWallPaper(id: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputWallPaperNoFile(_ reader: BufferReader) -> InputWallPaper? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return Api.InputWallPaper.inputWallPaperNoFile(id: _1!) - } - else { - return nil - } - } - public static func parse_inputWallPaperSlug(_ reader: BufferReader) -> InputWallPaper? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.InputWallPaper.inputWallPaperSlug(slug: _1!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum InputWebDocument: TypeConstructorDescription { - case inputWebDocument(url: String, size: Int32, mimeType: String, attributes: [Api.DocumentAttribute]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputWebDocument(let url, let size, let mimeType, let attributes): - if boxed { - buffer.appendInt32(-1678949555) - } - serializeString(url, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(attributes.count)) - for item in attributes { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputWebDocument(let url, let size, let mimeType, let attributes): - return ("inputWebDocument", [("url", url as Any), ("size", size as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any)]) - } - } - - public static func parse_inputWebDocument(_ reader: BufferReader) -> InputWebDocument? { - var _1: String? - _1 = parseString(reader) - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - var _4: [Api.DocumentAttribute]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DocumentAttribute.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputWebDocument.inputWebDocument(url: _1!, size: _2!, mimeType: _3!, attributes: _4!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index ecddafbffd8..c66b7ed17b2 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -1,3 +1,231 @@ +public extension Api { + indirect enum InputUser: TypeConstructorDescription { + case inputUser(userId: Int64, accessHash: Int64) + case inputUserEmpty + case inputUserFromMessage(peer: Api.InputPeer, msgId: Int32, userId: Int64) + case inputUserSelf + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputUser(let userId, let accessHash): + if boxed { + buffer.appendInt32(-233744186) + } + serializeInt64(userId, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputUserEmpty: + if boxed { + buffer.appendInt32(-1182234929) + } + + break + case .inputUserFromMessage(let peer, let msgId, let userId): + if boxed { + buffer.appendInt32(497305826) + } + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + serializeInt64(userId, buffer: buffer, boxed: false) + break + case .inputUserSelf: + if boxed { + buffer.appendInt32(-138301121) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputUser(let userId, let accessHash): + return ("inputUser", [("userId", userId as Any), ("accessHash", accessHash as Any)]) + case .inputUserEmpty: + return ("inputUserEmpty", []) + case .inputUserFromMessage(let peer, let msgId, let userId): + return ("inputUserFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("userId", userId as Any)]) + case .inputUserSelf: + return ("inputUserSelf", []) + } + } + + public static func parse_inputUser(_ reader: BufferReader) -> InputUser? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputUser.inputUser(userId: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputUserEmpty(_ reader: BufferReader) -> InputUser? { + return Api.InputUser.inputUserEmpty + } + public static func parse_inputUserFromMessage(_ reader: BufferReader) -> InputUser? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputUser.inputUserFromMessage(peer: _1!, msgId: _2!, userId: _3!) + } + else { + return nil + } + } + public static func parse_inputUserSelf(_ reader: BufferReader) -> InputUser? { + return Api.InputUser.inputUserSelf + } + + } +} +public extension Api { + enum InputWallPaper: TypeConstructorDescription { + case inputWallPaper(id: Int64, accessHash: Int64) + case inputWallPaperNoFile(id: Int64) + case inputWallPaperSlug(slug: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputWallPaper(let id, let accessHash): + if boxed { + buffer.appendInt32(-433014407) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputWallPaperNoFile(let id): + if boxed { + buffer.appendInt32(-1770371538) + } + serializeInt64(id, buffer: buffer, boxed: false) + break + case .inputWallPaperSlug(let slug): + if boxed { + buffer.appendInt32(1913199744) + } + serializeString(slug, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputWallPaper(let id, let accessHash): + return ("inputWallPaper", [("id", id as Any), ("accessHash", accessHash as Any)]) + case .inputWallPaperNoFile(let id): + return ("inputWallPaperNoFile", [("id", id as Any)]) + case .inputWallPaperSlug(let slug): + return ("inputWallPaperSlug", [("slug", slug as Any)]) + } + } + + public static func parse_inputWallPaper(_ reader: BufferReader) -> InputWallPaper? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputWallPaper.inputWallPaper(id: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputWallPaperNoFile(_ reader: BufferReader) -> InputWallPaper? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.InputWallPaper.inputWallPaperNoFile(id: _1!) + } + else { + return nil + } + } + public static func parse_inputWallPaperSlug(_ reader: BufferReader) -> InputWallPaper? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputWallPaper.inputWallPaperSlug(slug: _1!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum InputWebDocument: TypeConstructorDescription { + case inputWebDocument(url: String, size: Int32, mimeType: String, attributes: [Api.DocumentAttribute]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputWebDocument(let url, let size, let mimeType, let attributes): + if boxed { + buffer.appendInt32(-1678949555) + } + serializeString(url, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputWebDocument(let url, let size, let mimeType, let attributes): + return ("inputWebDocument", [("url", url as Any), ("size", size as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any)]) + } + } + + public static func parse_inputWebDocument(_ reader: BufferReader) -> InputWebDocument? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + var _4: [Api.DocumentAttribute]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DocumentAttribute.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputWebDocument.inputWebDocument(url: _1!, size: _2!, mimeType: _3!, attributes: _4!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputWebFileLocation: TypeConstructorDescription { case inputWebFileAudioAlbumThumbLocation(flags: Int32, document: Api.InputDocument?, title: String?, performer: String?) @@ -846,207 +1074,3 @@ public extension Api { } } -public extension Api { - enum KeyboardButtonRow: TypeConstructorDescription { - case keyboardButtonRow(buttons: [Api.KeyboardButton]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .keyboardButtonRow(let buttons): - if boxed { - buffer.appendInt32(2002815875) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(buttons.count)) - for item in buttons { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .keyboardButtonRow(let buttons): - return ("keyboardButtonRow", [("buttons", buttons as Any)]) - } - } - - public static func parse_keyboardButtonRow(_ reader: BufferReader) -> KeyboardButtonRow? { - var _1: [Api.KeyboardButton]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.KeyboardButton.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.KeyboardButtonRow.keyboardButtonRow(buttons: _1!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum LabeledPrice: TypeConstructorDescription { - case labeledPrice(label: String, amount: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .labeledPrice(let label, let amount): - if boxed { - buffer.appendInt32(-886477832) - } - serializeString(label, buffer: buffer, boxed: false) - serializeInt64(amount, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .labeledPrice(let label, let amount): - return ("labeledPrice", [("label", label as Any), ("amount", amount as Any)]) - } - } - - public static func parse_labeledPrice(_ reader: BufferReader) -> LabeledPrice? { - var _1: String? - _1 = parseString(reader) - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.LabeledPrice.labeledPrice(label: _1!, amount: _2!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum LangPackDifference: TypeConstructorDescription { - case langPackDifference(langCode: String, fromVersion: Int32, version: Int32, strings: [Api.LangPackString]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .langPackDifference(let langCode, let fromVersion, let version, let strings): - if boxed { - buffer.appendInt32(-209337866) - } - serializeString(langCode, buffer: buffer, boxed: false) - serializeInt32(fromVersion, buffer: buffer, boxed: false) - serializeInt32(version, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(strings.count)) - for item in strings { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .langPackDifference(let langCode, let fromVersion, let version, let strings): - return ("langPackDifference", [("langCode", langCode as Any), ("fromVersion", fromVersion as Any), ("version", version as Any), ("strings", strings as Any)]) - } - } - - public static func parse_langPackDifference(_ reader: BufferReader) -> LangPackDifference? { - var _1: String? - _1 = parseString(reader) - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: [Api.LangPackString]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.LangPackString.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.LangPackDifference.langPackDifference(langCode: _1!, fromVersion: _2!, version: _3!, strings: _4!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum LangPackLanguage: TypeConstructorDescription { - case langPackLanguage(flags: Int32, name: String, nativeName: String, langCode: String, baseLangCode: String?, pluralCode: String, stringsCount: Int32, translatedCount: Int32, translationsUrl: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .langPackLanguage(let flags, let name, let nativeName, let langCode, let baseLangCode, let pluralCode, let stringsCount, let translatedCount, let translationsUrl): - if boxed { - buffer.appendInt32(-288727837) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(name, buffer: buffer, boxed: false) - serializeString(nativeName, buffer: buffer, boxed: false) - serializeString(langCode, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeString(baseLangCode!, buffer: buffer, boxed: false)} - serializeString(pluralCode, buffer: buffer, boxed: false) - serializeInt32(stringsCount, buffer: buffer, boxed: false) - serializeInt32(translatedCount, buffer: buffer, boxed: false) - serializeString(translationsUrl, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .langPackLanguage(let flags, let name, let nativeName, let langCode, let baseLangCode, let pluralCode, let stringsCount, let translatedCount, let translationsUrl): - return ("langPackLanguage", [("flags", flags as Any), ("name", name as Any), ("nativeName", nativeName as Any), ("langCode", langCode as Any), ("baseLangCode", baseLangCode as Any), ("pluralCode", pluralCode as Any), ("stringsCount", stringsCount as Any), ("translatedCount", translatedCount as Any), ("translationsUrl", translationsUrl as Any)]) - } - } - - public static func parse_langPackLanguage(_ reader: BufferReader) -> LangPackLanguage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - var _4: String? - _4 = parseString(reader) - var _5: String? - if Int(_1!) & Int(1 << 1) != 0 {_5 = parseString(reader) } - var _6: String? - _6 = parseString(reader) - var _7: Int32? - _7 = reader.readInt32() - var _8: Int32? - _8 = reader.readInt32() - var _9: String? - _9 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - let _c9 = _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.LangPackLanguage.langPackLanguage(flags: _1!, name: _2!, nativeName: _3!, langCode: _4!, baseLangCode: _5, pluralCode: _6!, stringsCount: _7!, translatedCount: _8!, translationsUrl: _9!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index 737b1ea9331..6bdd0446fe4 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -1,3 +1,207 @@ +public extension Api { + enum KeyboardButtonRow: TypeConstructorDescription { + case keyboardButtonRow(buttons: [Api.KeyboardButton]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .keyboardButtonRow(let buttons): + if boxed { + buffer.appendInt32(2002815875) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(buttons.count)) + for item in buttons { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .keyboardButtonRow(let buttons): + return ("keyboardButtonRow", [("buttons", buttons as Any)]) + } + } + + public static func parse_keyboardButtonRow(_ reader: BufferReader) -> KeyboardButtonRow? { + var _1: [Api.KeyboardButton]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.KeyboardButton.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.KeyboardButtonRow.keyboardButtonRow(buttons: _1!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum LabeledPrice: TypeConstructorDescription { + case labeledPrice(label: String, amount: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .labeledPrice(let label, let amount): + if boxed { + buffer.appendInt32(-886477832) + } + serializeString(label, buffer: buffer, boxed: false) + serializeInt64(amount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .labeledPrice(let label, let amount): + return ("labeledPrice", [("label", label as Any), ("amount", amount as Any)]) + } + } + + public static func parse_labeledPrice(_ reader: BufferReader) -> LabeledPrice? { + var _1: String? + _1 = parseString(reader) + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.LabeledPrice.labeledPrice(label: _1!, amount: _2!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum LangPackDifference: TypeConstructorDescription { + case langPackDifference(langCode: String, fromVersion: Int32, version: Int32, strings: [Api.LangPackString]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .langPackDifference(let langCode, let fromVersion, let version, let strings): + if boxed { + buffer.appendInt32(-209337866) + } + serializeString(langCode, buffer: buffer, boxed: false) + serializeInt32(fromVersion, buffer: buffer, boxed: false) + serializeInt32(version, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(strings.count)) + for item in strings { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .langPackDifference(let langCode, let fromVersion, let version, let strings): + return ("langPackDifference", [("langCode", langCode as Any), ("fromVersion", fromVersion as Any), ("version", version as Any), ("strings", strings as Any)]) + } + } + + public static func parse_langPackDifference(_ reader: BufferReader) -> LangPackDifference? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: [Api.LangPackString]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.LangPackString.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.LangPackDifference.langPackDifference(langCode: _1!, fromVersion: _2!, version: _3!, strings: _4!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum LangPackLanguage: TypeConstructorDescription { + case langPackLanguage(flags: Int32, name: String, nativeName: String, langCode: String, baseLangCode: String?, pluralCode: String, stringsCount: Int32, translatedCount: Int32, translationsUrl: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .langPackLanguage(let flags, let name, let nativeName, let langCode, let baseLangCode, let pluralCode, let stringsCount, let translatedCount, let translationsUrl): + if boxed { + buffer.appendInt32(-288727837) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(name, buffer: buffer, boxed: false) + serializeString(nativeName, buffer: buffer, boxed: false) + serializeString(langCode, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeString(baseLangCode!, buffer: buffer, boxed: false)} + serializeString(pluralCode, buffer: buffer, boxed: false) + serializeInt32(stringsCount, buffer: buffer, boxed: false) + serializeInt32(translatedCount, buffer: buffer, boxed: false) + serializeString(translationsUrl, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .langPackLanguage(let flags, let name, let nativeName, let langCode, let baseLangCode, let pluralCode, let stringsCount, let translatedCount, let translationsUrl): + return ("langPackLanguage", [("flags", flags as Any), ("name", name as Any), ("nativeName", nativeName as Any), ("langCode", langCode as Any), ("baseLangCode", baseLangCode as Any), ("pluralCode", pluralCode as Any), ("stringsCount", stringsCount as Any), ("translatedCount", translatedCount as Any), ("translationsUrl", translationsUrl as Any)]) + } + } + + public static func parse_langPackLanguage(_ reader: BufferReader) -> LangPackLanguage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: String? + _4 = parseString(reader) + var _5: String? + if Int(_1!) & Int(1 << 1) != 0 {_5 = parseString(reader) } + var _6: String? + _6 = parseString(reader) + var _7: Int32? + _7 = reader.readInt32() + var _8: Int32? + _8 = reader.readInt32() + var _9: String? + _9 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.LangPackLanguage.langPackLanguage(flags: _1!, name: _2!, nativeName: _3!, langCode: _4!, baseLangCode: _5, pluralCode: _6!, stringsCount: _7!, translatedCount: _8!, translationsUrl: _9!) + } + else { + return nil + } + } + + } +} public extension Api { enum LangPackString: TypeConstructorDescription { case langPackString(key: String, value: String) @@ -163,6 +367,7 @@ public extension Api { case mediaAreaSuggestedReaction(flags: Int32, coordinates: Api.MediaAreaCoordinates, reaction: Api.Reaction) case mediaAreaUrl(coordinates: Api.MediaAreaCoordinates, url: String) case mediaAreaVenue(coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String) + case mediaAreaWeather(coordinates: Api.MediaAreaCoordinates, emoji: String, temperatureC: Double, color: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -226,6 +431,15 @@ public extension Api { serializeString(venueId, buffer: buffer, boxed: false) serializeString(venueType, buffer: buffer, boxed: false) break + case .mediaAreaWeather(let coordinates, let emoji, let temperatureC, let color): + if boxed { + buffer.appendInt32(1235637404) + } + coordinates.serialize(buffer, true) + serializeString(emoji, buffer: buffer, boxed: false) + serializeDouble(temperatureC, buffer: buffer, boxed: false) + serializeInt32(color, buffer: buffer, boxed: false) + break } } @@ -245,6 +459,8 @@ public extension Api { return ("mediaAreaUrl", [("coordinates", coordinates as Any), ("url", url as Any)]) case .mediaAreaVenue(let coordinates, let geo, let title, let address, let provider, let venueId, let venueType): return ("mediaAreaVenue", [("coordinates", coordinates as Any), ("geo", geo as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any)]) + case .mediaAreaWeather(let coordinates, let emoji, let temperatureC, let color): + return ("mediaAreaWeather", [("coordinates", coordinates as Any), ("emoji", emoji as Any), ("temperatureC", temperatureC as Any), ("color", color as Any)]) } } @@ -403,6 +619,28 @@ public extension Api { return nil } } + public static func parse_mediaAreaWeather(_ reader: BufferReader) -> MediaArea? { + var _1: Api.MediaAreaCoordinates? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates + } + var _2: String? + _2 = parseString(reader) + var _3: Double? + _3 = reader.readDouble() + 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.MediaArea.mediaAreaWeather(coordinates: _1!, emoji: _2!, temperatureC: _3!, color: _4!) + } + else { + return nil + } + } } } @@ -750,12 +988,14 @@ public extension Api { case messageActionGeoProximityReached(fromId: Api.Peer, toId: Api.Peer, distance: Int32) case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?) case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?) + case messageActionGiftStars(flags: Int32, currency: String, amount: Int64, stars: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) case messageActionGiveawayLaunch case messageActionGiveawayResults(winnersCount: Int32, unclaimedCount: Int32) case messageActionGroupCall(flags: Int32, call: Api.InputGroupCall, duration: Int32?) case messageActionGroupCallScheduled(call: Api.InputGroupCall, scheduleDate: Int32) case messageActionHistoryClear case messageActionInviteToGroupCall(call: Api.InputGroupCall, users: [Int64]) + case messageActionPaymentRefunded(flags: Int32, peer: Api.Peer, currency: String, totalAmount: Int64, payload: Buffer?, charge: Api.PaymentCharge) case messageActionPaymentSent(flags: Int32, currency: String, totalAmount: Int64, invoiceSlug: String?) case messageActionPaymentSentMe(flags: Int32, currency: String, totalAmount: Int64, payload: Buffer, info: Api.PaymentRequestedInfo?, shippingOptionId: String?, charge: Api.PaymentCharge) case messageActionPhoneCall(flags: Int32, callId: Int64, reason: Api.PhoneCallDiscardReason?, duration: Int32?) @@ -923,6 +1163,18 @@ public extension Api { if Int(flags) & Int(1 << 0) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)} break + case .messageActionGiftStars(let flags, let currency, let amount, let stars, let cryptoCurrency, let cryptoAmount, let transactionId): + if boxed { + buffer.appendInt32(1171632161) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(currency, buffer: buffer, boxed: false) + serializeInt64(amount, buffer: buffer, boxed: false) + serializeInt64(stars, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 0) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(transactionId!, buffer: buffer, boxed: false)} + break case .messageActionGiveawayLaunch: if boxed { buffer.appendInt32(858499565) @@ -968,6 +1220,17 @@ public extension Api { serializeInt64(item, buffer: buffer, boxed: false) } break + case .messageActionPaymentRefunded(let flags, let peer, let currency, let totalAmount, let payload, let charge): + if boxed { + buffer.appendInt32(1102307842) + } + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeString(currency, buffer: buffer, boxed: false) + serializeInt64(totalAmount, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeBytes(payload!, buffer: buffer, boxed: false)} + charge.serialize(buffer, true) + break case .messageActionPaymentSent(let flags, let currency, let totalAmount, let invoiceSlug): if boxed { buffer.appendInt32(-1776926890) @@ -1157,6 +1420,8 @@ public extension Api { return ("messageActionGiftCode", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("months", months as Any), ("slug", slug as Any), ("currency", currency as Any), ("amount", amount as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)]) case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount): return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)]) + case .messageActionGiftStars(let flags, let currency, let amount, let stars, let cryptoCurrency, let cryptoAmount, let transactionId): + return ("messageActionGiftStars", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("stars", stars as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("transactionId", transactionId as Any)]) case .messageActionGiveawayLaunch: return ("messageActionGiveawayLaunch", []) case .messageActionGiveawayResults(let winnersCount, let unclaimedCount): @@ -1169,6 +1434,8 @@ public extension Api { return ("messageActionHistoryClear", []) case .messageActionInviteToGroupCall(let call, let users): return ("messageActionInviteToGroupCall", [("call", call as Any), ("users", users as Any)]) + case .messageActionPaymentRefunded(let flags, let peer, let currency, let totalAmount, let payload, let charge): + return ("messageActionPaymentRefunded", [("flags", flags as Any), ("peer", peer as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("payload", payload as Any), ("charge", charge as Any)]) case .messageActionPaymentSent(let flags, let currency, let totalAmount, let invoiceSlug): return ("messageActionPaymentSent", [("flags", flags as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("invoiceSlug", invoiceSlug as Any)]) case .messageActionPaymentSentMe(let flags, let currency, let totalAmount, let payload, let info, let shippingOptionId, let charge): @@ -1465,6 +1732,35 @@ public extension Api { return nil } } + public static func parse_messageActionGiftStars(_ reader: BufferReader) -> MessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int64? + _3 = reader.readInt64() + var _4: Int64? + _4 = reader.readInt64() + var _5: String? + if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) } + var _6: Int64? + if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt64() } + var _7: String? + if Int(_1!) & Int(1 << 1) != 0 {_7 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.MessageAction.messageActionGiftStars(flags: _1!, currency: _2!, amount: _3!, stars: _4!, cryptoCurrency: _5, cryptoAmount: _6, transactionId: _7) + } + else { + return nil + } + } public static func parse_messageActionGiveawayLaunch(_ reader: BufferReader) -> MessageAction? { return Api.MessageAction.messageActionGiveawayLaunch } @@ -1538,6 +1834,36 @@ public extension Api { return nil } } + public static func parse_messageActionPaymentRefunded(_ reader: BufferReader) -> MessageAction? { + 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: String? + _3 = parseString(reader) + var _4: Int64? + _4 = reader.readInt64() + var _5: Buffer? + if Int(_1!) & Int(1 << 0) != 0 {_5 = parseBytes(reader) } + var _6: Api.PaymentCharge? + if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.PaymentCharge + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.MessageAction.messageActionPaymentRefunded(flags: _1!, peer: _2!, currency: _3!, totalAmount: _4!, payload: _5, charge: _6!) + } + else { + return nil + } + } public static func parse_messageActionPaymentSent(_ reader: BufferReader) -> MessageAction? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 51d1fb1d7fe..8855da4143b 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -724,6 +724,48 @@ public extension Api { } } +public extension Api { + indirect enum BotPreviewMedia: TypeConstructorDescription { + case botPreviewMedia(date: Int32, media: Api.MessageMedia) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .botPreviewMedia(let date, let media): + if boxed { + buffer.appendInt32(602479523) + } + serializeInt32(date, buffer: buffer, boxed: false) + media.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .botPreviewMedia(let date, let media): + return ("botPreviewMedia", [("date", date as Any), ("media", media as Any)]) + } + } + + public static func parse_botPreviewMedia(_ reader: BufferReader) -> BotPreviewMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.MessageMedia? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.MessageMedia + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.BotPreviewMedia.botPreviewMedia(date: _1!, media: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum BroadcastRevenueBalances: TypeConstructorDescription { case broadcastRevenueBalances(currentBalance: Int64, availableBalance: Int64, overallRevenue: Int64) @@ -1160,53 +1202,3 @@ public extension Api { } } -public extension Api { - enum BusinessIntro: TypeConstructorDescription { - case businessIntro(flags: Int32, title: String, description: String, sticker: Api.Document?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .businessIntro(let flags, let title, let description, let sticker): - if boxed { - buffer.appendInt32(1510606445) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - serializeString(description, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {sticker!.serialize(buffer, true)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .businessIntro(let flags, let title, let description, let sticker): - return ("businessIntro", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("sticker", sticker as Any)]) - } - } - - public static func parse_businessIntro(_ reader: BufferReader) -> BusinessIntro? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - var _4: Api.Document? - if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.Document - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.BusinessIntro.businessIntro(flags: _1!, title: _2!, description: _3!, sticker: _4) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index 3a9094cca90..0d646cf4e46 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -566,6 +566,58 @@ public extension Api { } } +public extension Api { + enum StarsGiftOption: TypeConstructorDescription { + case starsGiftOption(flags: Int32, stars: Int64, storeProduct: String?, currency: String, amount: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsGiftOption(let flags, let stars, let storeProduct, let currency, let amount): + if boxed { + buffer.appendInt32(1577421297) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(stars, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(storeProduct!, buffer: buffer, boxed: false)} + serializeString(currency, buffer: buffer, boxed: false) + serializeInt64(amount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsGiftOption(let flags, let stars, let storeProduct, let currency, let amount): + return ("starsGiftOption", [("flags", flags as Any), ("stars", stars as Any), ("storeProduct", storeProduct as Any), ("currency", currency as Any), ("amount", amount as Any)]) + } + } + + public static func parse_starsGiftOption(_ reader: BufferReader) -> StarsGiftOption? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: String? + if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } + var _4: String? + _4 = parseString(reader) + var _5: Int64? + _5 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.StarsGiftOption.starsGiftOption(flags: _1!, stars: _2!, storeProduct: _3, currency: _4!, amount: _5!) + } + else { + return nil + } + } + + } +} public extension Api { enum StarsRevenueStatus: TypeConstructorDescription { case starsRevenueStatus(flags: Int32, currentBalance: Int64, availableBalance: Int64, overallRevenue: Int64, nextWithdrawalAt: Int32?) diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 7d031ef3001..ec78a2243ac 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -86,6 +86,7 @@ public extension Api { } public extension Api { enum TopPeerCategory: TypeConstructorDescription { + case topPeerCategoryBotsApp case topPeerCategoryBotsInline case topPeerCategoryBotsPM case topPeerCategoryChannels @@ -97,6 +98,12 @@ public extension Api { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .topPeerCategoryBotsApp: + if boxed { + buffer.appendInt32(-39945236) + } + + break case .topPeerCategoryBotsInline: if boxed { buffer.appendInt32(344356834) @@ -150,6 +157,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { + case .topPeerCategoryBotsApp: + return ("topPeerCategoryBotsApp", []) case .topPeerCategoryBotsInline: return ("topPeerCategoryBotsInline", []) case .topPeerCategoryBotsPM: @@ -169,6 +178,9 @@ public extension Api { } } + public static func parse_topPeerCategoryBotsApp(_ reader: BufferReader) -> TopPeerCategory? { + return Api.TopPeerCategory.topPeerCategoryBotsApp + } public static func parse_topPeerCategoryBotsInline(_ reader: BufferReader) -> TopPeerCategory? { return Api.TopPeerCategory.topPeerCategoryBotsInline } diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index e09e22c1644..07aeb17b3be 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -452,14 +452,14 @@ public extension Api { } public extension Api { enum User: TypeConstructorDescription { - case user(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, firstName: String?, lastName: String?, username: String?, phone: String?, photo: Api.UserProfilePhoto?, status: Api.UserStatus?, botInfoVersion: Int32?, restrictionReason: [Api.RestrictionReason]?, botInlinePlaceholder: String?, langCode: String?, emojiStatus: Api.EmojiStatus?, usernames: [Api.Username]?, storiesMaxId: Int32?, color: Api.PeerColor?, profileColor: Api.PeerColor?) + case user(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, firstName: String?, lastName: String?, username: String?, phone: String?, photo: Api.UserProfilePhoto?, status: Api.UserStatus?, botInfoVersion: Int32?, restrictionReason: [Api.RestrictionReason]?, botInlinePlaceholder: String?, langCode: String?, emojiStatus: Api.EmojiStatus?, usernames: [Api.Username]?, storiesMaxId: Int32?, color: Api.PeerColor?, profileColor: Api.PeerColor?, botActiveUsers: Int32?) case userEmpty(id: Int64) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId, let color, let profileColor): + case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId, let color, let profileColor, let botActiveUsers): if boxed { - buffer.appendInt32(559694904) + buffer.appendInt32(-2093920310) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags2, buffer: buffer, boxed: false) @@ -488,6 +488,7 @@ public extension Api { if Int(flags2) & Int(1 << 5) != 0 {serializeInt32(storiesMaxId!, buffer: buffer, boxed: false)} if Int(flags2) & Int(1 << 8) != 0 {color!.serialize(buffer, true)} if Int(flags2) & Int(1 << 9) != 0 {profileColor!.serialize(buffer, true)} + if Int(flags2) & Int(1 << 12) != 0 {serializeInt32(botActiveUsers!, buffer: buffer, boxed: false)} break case .userEmpty(let id): if boxed { @@ -500,8 +501,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId, let color, let profileColor): - return ("user", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("username", username as Any), ("phone", phone as Any), ("photo", photo as Any), ("status", status as Any), ("botInfoVersion", botInfoVersion as Any), ("restrictionReason", restrictionReason as Any), ("botInlinePlaceholder", botInlinePlaceholder as Any), ("langCode", langCode as Any), ("emojiStatus", emojiStatus as Any), ("usernames", usernames as Any), ("storiesMaxId", storiesMaxId as Any), ("color", color as Any), ("profileColor", profileColor as Any)]) + case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId, let color, let profileColor, let botActiveUsers): + return ("user", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("username", username as Any), ("phone", phone as Any), ("photo", photo as Any), ("status", status as Any), ("botInfoVersion", botInfoVersion as Any), ("restrictionReason", restrictionReason as Any), ("botInlinePlaceholder", botInlinePlaceholder as Any), ("langCode", langCode as Any), ("emojiStatus", emojiStatus as Any), ("usernames", usernames as Any), ("storiesMaxId", storiesMaxId as Any), ("color", color as Any), ("profileColor", profileColor as Any), ("botActiveUsers", botActiveUsers as Any)]) case .userEmpty(let id): return ("userEmpty", [("id", id as Any)]) } @@ -560,6 +561,8 @@ public extension Api { if Int(_2!) & Int(1 << 9) != 0 {if let signature = reader.readInt32() { _19 = Api.parse(reader, signature: signature) as? Api.PeerColor } } + var _20: Int32? + if Int(_2!) & Int(1 << 12) != 0 {_20 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -579,8 +582,9 @@ public extension Api { let _c17 = (Int(_2!) & Int(1 << 5) == 0) || _17 != nil let _c18 = (Int(_2!) & Int(1 << 8) == 0) || _18 != nil let _c19 = (Int(_2!) & Int(1 << 9) == 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.User.user(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, firstName: _5, lastName: _6, username: _7, phone: _8, photo: _9, status: _10, botInfoVersion: _11, restrictionReason: _12, botInlinePlaceholder: _13, langCode: _14, emojiStatus: _15, usernames: _16, storiesMaxId: _17, color: _18, profileColor: _19) + let _c20 = (Int(_2!) & Int(1 << 12) == 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.User.user(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, firstName: _5, lastName: _6, username: _7, phone: _8, photo: _9, status: _10, botInfoVersion: _11, restrictionReason: _12, botInlinePlaceholder: _13, langCode: _14, emojiStatus: _15, usernames: _16, storiesMaxId: _17, color: _18, profileColor: _19, botActiveUsers: _20) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index 29cc8fa57a0..7f9b44e542b 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -920,26 +920,18 @@ public extension Api.bots { } } -public extension Api.channels { - enum AdminLogResults: TypeConstructorDescription { - case adminLogResults(events: [Api.ChannelAdminLogEvent], chats: [Api.Chat], users: [Api.User]) +public extension Api.bots { + enum PopularAppBots: TypeConstructorDescription { + case popularAppBots(flags: Int32, nextOffset: String?, users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .adminLogResults(let events, let chats, let users): + case .popularAppBots(let flags, let nextOffset, let users): if boxed { - buffer.appendInt32(-309659827) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(events.count)) - for item in events { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) + buffer.appendInt32(428978491) } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(users.count)) for item in users { @@ -951,29 +943,25 @@ public extension Api.channels { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .adminLogResults(let events, let chats, let users): - return ("adminLogResults", [("events", events as Any), ("chats", chats as Any), ("users", users as Any)]) + case .popularAppBots(let flags, let nextOffset, let users): + return ("popularAppBots", [("flags", flags as Any), ("nextOffset", nextOffset as Any), ("users", users as Any)]) } } - public static func parse_adminLogResults(_ reader: BufferReader) -> AdminLogResults? { - var _1: [Api.ChannelAdminLogEvent]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ChannelAdminLogEvent.self) - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } + public static func parse_popularAppBots(_ reader: BufferReader) -> PopularAppBots? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) } var _3: [Api.User]? if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil - let _c2 = _2 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil let _c3 = _3 != nil if _c1 && _c2 && _c3 { - return Api.channels.AdminLogResults.adminLogResults(events: _1!, chats: _2!, users: _3!) + return Api.bots.PopularAppBots.popularAppBots(flags: _1!, nextOffset: _2, users: _3!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index 04914ae0cb9..36dea196a1a 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -1,3 +1,117 @@ +public extension Api.bots { + enum PreviewInfo: TypeConstructorDescription { + case previewInfo(media: [Api.BotPreviewMedia], langCodes: [String]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .previewInfo(let media, let langCodes): + if boxed { + buffer.appendInt32(212278628) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(media.count)) + for item in media { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(langCodes.count)) + for item in langCodes { + serializeString(item, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .previewInfo(let media, let langCodes): + return ("previewInfo", [("media", media as Any), ("langCodes", langCodes as Any)]) + } + } + + public static func parse_previewInfo(_ reader: BufferReader) -> PreviewInfo? { + var _1: [Api.BotPreviewMedia]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotPreviewMedia.self) + } + var _2: [String]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.bots.PreviewInfo.previewInfo(media: _1!, langCodes: _2!) + } + else { + return nil + } + } + + } +} +public extension Api.channels { + enum AdminLogResults: TypeConstructorDescription { + case adminLogResults(events: [Api.ChannelAdminLogEvent], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .adminLogResults(let events, let chats, let users): + if boxed { + buffer.appendInt32(-309659827) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(events.count)) + for item in events { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .adminLogResults(let events, let chats, let users): + return ("adminLogResults", [("events", events as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_adminLogResults(_ reader: BufferReader) -> AdminLogResults? { + var _1: [Api.ChannelAdminLogEvent]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ChannelAdminLogEvent.self) + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.channels.AdminLogResults.adminLogResults(events: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } +} public extension Api.channels { enum ChannelParticipant: TypeConstructorDescription { case channelParticipant(participant: Api.ChannelParticipant, chats: [Api.Chat], users: [Api.User]) @@ -1342,121 +1456,3 @@ public extension Api.help { } } -public extension Api.help { - enum Country: TypeConstructorDescription { - case country(flags: Int32, iso2: String, defaultName: String, name: String?, countryCodes: [Api.help.CountryCode]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .country(let flags, let iso2, let defaultName, let name, let countryCodes): - if boxed { - buffer.appendInt32(-1014526429) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(iso2, buffer: buffer, boxed: false) - serializeString(defaultName, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeString(name!, buffer: buffer, boxed: false)} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(countryCodes.count)) - for item in countryCodes { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .country(let flags, let iso2, let defaultName, let name, let countryCodes): - return ("country", [("flags", flags as Any), ("iso2", iso2 as Any), ("defaultName", defaultName as Any), ("name", name as Any), ("countryCodes", countryCodes as Any)]) - } - } - - public static func parse_country(_ reader: BufferReader) -> Country? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - var _4: String? - if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } - var _5: [Api.help.CountryCode]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.help.CountryCode.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.help.Country.country(flags: _1!, iso2: _2!, defaultName: _3!, name: _4, countryCodes: _5!) - } - else { - return nil - } - } - - } -} -public extension Api.help { - enum CountryCode: TypeConstructorDescription { - case countryCode(flags: Int32, countryCode: String, prefixes: [String]?, patterns: [String]?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .countryCode(let flags, let countryCode, let prefixes, let patterns): - if boxed { - buffer.appendInt32(1107543535) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(countryCode, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(prefixes!.count)) - for item in prefixes! { - serializeString(item, buffer: buffer, boxed: false) - }} - if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(patterns!.count)) - for item in patterns! { - serializeString(item, buffer: buffer, boxed: false) - }} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .countryCode(let flags, let countryCode, let prefixes, let patterns): - return ("countryCode", [("flags", flags as Any), ("countryCode", countryCode as Any), ("prefixes", prefixes as Any), ("patterns", patterns as Any)]) - } - } - - public static func parse_countryCode(_ reader: BufferReader) -> CountryCode? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: [String]? - if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) - } } - var _4: [String]? - if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.help.CountryCode.countryCode(flags: _1!, countryCode: _2!, prefixes: _3, patterns: _4) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 331ac6b496a..3ec0b4d752c 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -1,3 +1,53 @@ +public extension Api { + enum BusinessIntro: TypeConstructorDescription { + case businessIntro(flags: Int32, title: String, description: String, sticker: Api.Document?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessIntro(let flags, let title, let description, let sticker): + if boxed { + buffer.appendInt32(1510606445) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(description, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {sticker!.serialize(buffer, true)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessIntro(let flags, let title, let description, let sticker): + return ("businessIntro", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("sticker", sticker as Any)]) + } + } + + public static func parse_businessIntro(_ reader: BufferReader) -> BusinessIntro? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: Api.Document? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.Document + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.BusinessIntro.businessIntro(flags: _1!, title: _2!, description: _3!, sticker: _4) + } + else { + return nil + } + } + + } +} public extension Api { enum BusinessLocation: TypeConstructorDescription { case businessLocation(flags: Int32, geoPoint: Api.GeoPoint?, address: String) diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index fd1655b7137..8156e1ba080 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -1,3 +1,121 @@ +public extension Api.help { + enum Country: TypeConstructorDescription { + case country(flags: Int32, iso2: String, defaultName: String, name: String?, countryCodes: [Api.help.CountryCode]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .country(let flags, let iso2, let defaultName, let name, let countryCodes): + if boxed { + buffer.appendInt32(-1014526429) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(iso2, buffer: buffer, boxed: false) + serializeString(defaultName, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeString(name!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(countryCodes.count)) + for item in countryCodes { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .country(let flags, let iso2, let defaultName, let name, let countryCodes): + return ("country", [("flags", flags as Any), ("iso2", iso2 as Any), ("defaultName", defaultName as Any), ("name", name as Any), ("countryCodes", countryCodes as Any)]) + } + } + + public static func parse_country(_ reader: BufferReader) -> Country? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: String? + if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } + var _5: [Api.help.CountryCode]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.help.CountryCode.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.help.Country.country(flags: _1!, iso2: _2!, defaultName: _3!, name: _4, countryCodes: _5!) + } + else { + return nil + } + } + + } +} +public extension Api.help { + enum CountryCode: TypeConstructorDescription { + case countryCode(flags: Int32, countryCode: String, prefixes: [String]?, patterns: [String]?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .countryCode(let flags, let countryCode, let prefixes, let patterns): + if boxed { + buffer.appendInt32(1107543535) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(countryCode, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(prefixes!.count)) + for item in prefixes! { + serializeString(item, buffer: buffer, boxed: false) + }} + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(patterns!.count)) + for item in patterns! { + serializeString(item, buffer: buffer, boxed: false) + }} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .countryCode(let flags, let countryCode, let prefixes, let patterns): + return ("countryCode", [("flags", flags as Any), ("countryCode", countryCode as Any), ("prefixes", prefixes as Any), ("patterns", patterns as Any)]) + } + } + + public static func parse_countryCode(_ reader: BufferReader) -> CountryCode? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: [String]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) + } } + var _4: [String]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.help.CountryCode.countryCode(flags: _1!, countryCode: _2!, prefixes: _3, patterns: _4) + } + else { + return nil + } + } + + } +} public extension Api.help { enum DeepLinkInfo: TypeConstructorDescription { case deepLinkInfo(flags: Int32, message: String, entities: [Api.MessageEntity]?) @@ -1232,61 +1350,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum AvailableReactions: TypeConstructorDescription { - case availableReactions(hash: Int32, reactions: [Api.AvailableReaction]) - case availableReactionsNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .availableReactions(let hash, let reactions): - if boxed { - buffer.appendInt32(1989032621) - } - serializeInt32(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(reactions.count)) - for item in reactions { - item.serialize(buffer, true) - } - break - case .availableReactionsNotModified: - if boxed { - buffer.appendInt32(-1626924713) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .availableReactions(let hash, let reactions): - return ("availableReactions", [("hash", hash as Any), ("reactions", reactions as Any)]) - case .availableReactionsNotModified: - return ("availableReactionsNotModified", []) - } - } - - public static func parse_availableReactions(_ reader: BufferReader) -> AvailableReactions? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.AvailableReaction]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.AvailableReaction.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.AvailableReactions.availableReactions(hash: _1!, reactions: _2!) - } - else { - return nil - } - } - public static func parse_availableReactionsNotModified(_ reader: BufferReader) -> AvailableReactions? { - return Api.messages.AvailableReactions.availableReactionsNotModified - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index 5b9410024d6..56ce04bdea8 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -1,3 +1,61 @@ +public extension Api.messages { + enum AvailableReactions: TypeConstructorDescription { + case availableReactions(hash: Int32, reactions: [Api.AvailableReaction]) + case availableReactionsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .availableReactions(let hash, let reactions): + if boxed { + buffer.appendInt32(1989032621) + } + serializeInt32(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(reactions.count)) + for item in reactions { + item.serialize(buffer, true) + } + break + case .availableReactionsNotModified: + if boxed { + buffer.appendInt32(-1626924713) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .availableReactions(let hash, let reactions): + return ("availableReactions", [("hash", hash as Any), ("reactions", reactions as Any)]) + case .availableReactionsNotModified: + return ("availableReactionsNotModified", []) + } + } + + public static func parse_availableReactions(_ reader: BufferReader) -> AvailableReactions? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.AvailableReaction]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.AvailableReaction.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.AvailableReactions.availableReactions(hash: _1!, reactions: _2!) + } + else { + return nil + } + } + public static func parse_availableReactionsNotModified(_ reader: BufferReader) -> AvailableReactions? { + return Api.messages.AvailableReactions.availableReactionsNotModified + } + + } +} public extension Api.messages { enum BotApp: TypeConstructorDescription { case botApp(flags: Int32, app: Api.BotApp) @@ -1372,105 +1430,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum HistoryImportParsed: TypeConstructorDescription { - case historyImportParsed(flags: Int32, title: String?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .historyImportParsed(let flags, let title): - if boxed { - buffer.appendInt32(1578088377) - } - serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 2) != 0 {serializeString(title!, buffer: buffer, boxed: false)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .historyImportParsed(let flags, let title): - return ("historyImportParsed", [("flags", flags as Any), ("title", title as Any)]) - } - } - - public static func parse_historyImportParsed(_ reader: BufferReader) -> HistoryImportParsed? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - if Int(_1!) & Int(1 << 2) != 0 {_2 = parseString(reader) } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 2) == 0) || _2 != nil - if _c1 && _c2 { - return Api.messages.HistoryImportParsed.historyImportParsed(flags: _1!, title: _2) - } - else { - return nil - } - } - - } -} -public extension Api.messages { - enum InactiveChats: TypeConstructorDescription { - case inactiveChats(dates: [Int32], chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inactiveChats(let dates, let chats, let users): - if boxed { - buffer.appendInt32(-1456996667) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(dates.count)) - for item in dates { - serializeInt32(item, buffer: buffer, boxed: false) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inactiveChats(let dates, let chats, let users): - return ("inactiveChats", [("dates", dates as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_inactiveChats(_ reader: BufferReader) -> InactiveChats? { - var _1: [Int32]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.InactiveChats.inactiveChats(dates: _1!, chats: _2!, users: _3!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api32.swift b/submodules/TelegramApi/Sources/Api32.swift index 6cdc8ecbd81..d31e82a113b 100644 --- a/submodules/TelegramApi/Sources/Api32.swift +++ b/submodules/TelegramApi/Sources/Api32.swift @@ -1,3 +1,105 @@ +public extension Api.messages { + enum HistoryImportParsed: TypeConstructorDescription { + case historyImportParsed(flags: Int32, title: String?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .historyImportParsed(let flags, let title): + if boxed { + buffer.appendInt32(1578088377) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {serializeString(title!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .historyImportParsed(let flags, let title): + return ("historyImportParsed", [("flags", flags as Any), ("title", title as Any)]) + } + } + + public static func parse_historyImportParsed(_ reader: BufferReader) -> HistoryImportParsed? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + if Int(_1!) & Int(1 << 2) != 0 {_2 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 2) == 0) || _2 != nil + if _c1 && _c2 { + return Api.messages.HistoryImportParsed.historyImportParsed(flags: _1!, title: _2) + } + else { + return nil + } + } + + } +} +public extension Api.messages { + enum InactiveChats: TypeConstructorDescription { + case inactiveChats(dates: [Int32], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inactiveChats(let dates, let chats, let users): + if boxed { + buffer.appendInt32(-1456996667) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(dates.count)) + for item in dates { + serializeInt32(item, buffer: buffer, boxed: false) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inactiveChats(let dates, let chats, let users): + return ("inactiveChats", [("dates", dates as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_inactiveChats(_ reader: BufferReader) -> InactiveChats? { + var _1: [Int32]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.InactiveChats.inactiveChats(dates: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } +} public extension Api.messages { indirect enum InvitedUsers: TypeConstructorDescription { case invitedUsers(updates: Api.Updates, missingInvitees: [Api.MissingInvitee]) @@ -1432,83 +1534,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum StickerSet: TypeConstructorDescription { - case stickerSet(set: Api.StickerSet, packs: [Api.StickerPack], keywords: [Api.StickerKeyword], documents: [Api.Document]) - case stickerSetNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .stickerSet(let set, let packs, let keywords, let documents): - if boxed { - buffer.appendInt32(1846886166) - } - set.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(packs.count)) - for item in packs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(keywords.count)) - for item in keywords { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(documents.count)) - for item in documents { - item.serialize(buffer, true) - } - break - case .stickerSetNotModified: - if boxed { - buffer.appendInt32(-738646805) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .stickerSet(let set, let packs, let keywords, let documents): - return ("stickerSet", [("set", set as Any), ("packs", packs as Any), ("keywords", keywords as Any), ("documents", documents as Any)]) - case .stickerSetNotModified: - return ("stickerSetNotModified", []) - } - } - - public static func parse_stickerSet(_ reader: BufferReader) -> StickerSet? { - var _1: Api.StickerSet? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StickerSet - } - var _2: [Api.StickerPack]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) - } - var _3: [Api.StickerKeyword]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerKeyword.self) - } - var _4: [Api.Document]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.messages.StickerSet.stickerSet(set: _1!, packs: _2!, keywords: _3!, documents: _4!) - } - else { - return nil - } - } - public static func parse_stickerSetNotModified(_ reader: BufferReader) -> StickerSet? { - return Api.messages.StickerSet.stickerSetNotModified - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api33.swift b/submodules/TelegramApi/Sources/Api33.swift index b8429bdcdcc..508a3b6a355 100644 --- a/submodules/TelegramApi/Sources/Api33.swift +++ b/submodules/TelegramApi/Sources/Api33.swift @@ -1,3 +1,83 @@ +public extension Api.messages { + enum StickerSet: TypeConstructorDescription { + case stickerSet(set: Api.StickerSet, packs: [Api.StickerPack], keywords: [Api.StickerKeyword], documents: [Api.Document]) + case stickerSetNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .stickerSet(let set, let packs, let keywords, let documents): + if boxed { + buffer.appendInt32(1846886166) + } + set.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(packs.count)) + for item in packs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(keywords.count)) + for item in keywords { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(documents.count)) + for item in documents { + item.serialize(buffer, true) + } + break + case .stickerSetNotModified: + if boxed { + buffer.appendInt32(-738646805) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .stickerSet(let set, let packs, let keywords, let documents): + return ("stickerSet", [("set", set as Any), ("packs", packs as Any), ("keywords", keywords as Any), ("documents", documents as Any)]) + case .stickerSetNotModified: + return ("stickerSetNotModified", []) + } + } + + public static func parse_stickerSet(_ reader: BufferReader) -> StickerSet? { + var _1: Api.StickerSet? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StickerSet + } + var _2: [Api.StickerPack]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) + } + var _3: [Api.StickerKeyword]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerKeyword.self) + } + var _4: [Api.Document]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.StickerSet.stickerSet(set: _1!, packs: _2!, keywords: _3!, documents: _4!) + } + else { + return nil + } + } + public static func parse_stickerSetNotModified(_ reader: BufferReader) -> StickerSet? { + return Api.messages.StickerSet.stickerSetNotModified + } + + } +} public extension Api.messages { enum StickerSetInstallResult: TypeConstructorDescription { case stickerSetInstallResultArchive(sets: [Api.StickerSetCovered]) @@ -1552,113 +1632,3 @@ public extension Api.phone { } } -public extension Api.phone { - enum JoinAsPeers: TypeConstructorDescription { - case joinAsPeers(peers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .joinAsPeers(let peers, let chats, let users): - if boxed { - buffer.appendInt32(-1343921601) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(peers.count)) - for item in peers { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .joinAsPeers(let peers, let chats, let users): - return ("joinAsPeers", [("peers", peers as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_joinAsPeers(_ reader: BufferReader) -> JoinAsPeers? { - var _1: [Api.Peer]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.phone.JoinAsPeers.joinAsPeers(peers: _1!, chats: _2!, users: _3!) - } - else { - return nil - } - } - - } -} -public extension Api.phone { - enum PhoneCall: TypeConstructorDescription { - case phoneCall(phoneCall: Api.PhoneCall, users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .phoneCall(let phoneCall, let users): - if boxed { - buffer.appendInt32(-326966976) - } - phoneCall.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .phoneCall(let phoneCall, let users): - return ("phoneCall", [("phoneCall", phoneCall as Any), ("users", users as Any)]) - } - } - - public static func parse_phoneCall(_ reader: BufferReader) -> PhoneCall? { - var _1: Api.PhoneCall? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.PhoneCall - } - var _2: [Api.User]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.phone.PhoneCall.phoneCall(phoneCall: _1!, users: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api34.swift b/submodules/TelegramApi/Sources/Api34.swift index 35e27005456..1e10a978c45 100644 --- a/submodules/TelegramApi/Sources/Api34.swift +++ b/submodules/TelegramApi/Sources/Api34.swift @@ -1,3 +1,113 @@ +public extension Api.phone { + enum JoinAsPeers: TypeConstructorDescription { + case joinAsPeers(peers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .joinAsPeers(let peers, let chats, let users): + if boxed { + buffer.appendInt32(-1343921601) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(peers.count)) + for item in peers { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .joinAsPeers(let peers, let chats, let users): + return ("joinAsPeers", [("peers", peers as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_joinAsPeers(_ reader: BufferReader) -> JoinAsPeers? { + var _1: [Api.Peer]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.phone.JoinAsPeers.joinAsPeers(peers: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } +} +public extension Api.phone { + enum PhoneCall: TypeConstructorDescription { + case phoneCall(phoneCall: Api.PhoneCall, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .phoneCall(let phoneCall, let users): + if boxed { + buffer.appendInt32(-326966976) + } + phoneCall.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .phoneCall(let phoneCall, let users): + return ("phoneCall", [("phoneCall", phoneCall as Any), ("users", users as Any)]) + } + } + + public static func parse_phoneCall(_ reader: BufferReader) -> PhoneCall? { + var _1: Api.PhoneCall? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.PhoneCall + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.phone.PhoneCall.phoneCall(phoneCall: _1!, users: _2!) + } + else { + return nil + } + } + + } +} public extension Api.photos { enum Photo: TypeConstructorDescription { case photo(photo: Api.Photo, users: [Api.User]) @@ -1426,141 +1536,3 @@ public extension Api.stories { } } -public extension Api.stories { - enum PeerStories: TypeConstructorDescription { - case peerStories(stories: Api.PeerStories, chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .peerStories(let stories, let chats, let users): - if boxed { - buffer.appendInt32(-890861720) - } - stories.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .peerStories(let stories, let chats, let users): - return ("peerStories", [("stories", stories as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_peerStories(_ reader: BufferReader) -> PeerStories? { - var _1: Api.PeerStories? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.PeerStories - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.stories.PeerStories.peerStories(stories: _1!, chats: _2!, users: _3!) - } - else { - return nil - } - } - - } -} -public extension Api.stories { - enum Stories: TypeConstructorDescription { - case stories(flags: Int32, count: Int32, stories: [Api.StoryItem], pinnedToTop: [Int32]?, chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .stories(let flags, let count, let stories, let pinnedToTop, let chats, let users): - if boxed { - buffer.appendInt32(1673780490) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(stories.count)) - for item in stories { - item.serialize(buffer, true) - } - if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(pinnedToTop!.count)) - for item in pinnedToTop! { - serializeInt32(item, buffer: buffer, boxed: false) - }} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .stories(let flags, let count, let stories, let pinnedToTop, let chats, let users): - return ("stories", [("flags", flags as Any), ("count", count as Any), ("stories", stories as Any), ("pinnedToTop", pinnedToTop as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_stories(_ reader: BufferReader) -> Stories? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.StoryItem]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryItem.self) - } - var _4: [Int32]? - if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) - } } - var _5: [Api.Chat]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _6: [Api.User]? - if let _ = reader.readInt32() { - _6 = 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.stories.Stories.stories(flags: _1!, count: _2!, stories: _3!, pinnedToTop: _4, chats: _5!, users: _6!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api35.swift b/submodules/TelegramApi/Sources/Api35.swift index c3cabdd4c7e..0e9dfa8a858 100644 --- a/submodules/TelegramApi/Sources/Api35.swift +++ b/submodules/TelegramApi/Sources/Api35.swift @@ -1,3 +1,141 @@ +public extension Api.stories { + enum PeerStories: TypeConstructorDescription { + case peerStories(stories: Api.PeerStories, chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .peerStories(let stories, let chats, let users): + if boxed { + buffer.appendInt32(-890861720) + } + stories.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .peerStories(let stories, let chats, let users): + return ("peerStories", [("stories", stories as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_peerStories(_ reader: BufferReader) -> PeerStories? { + var _1: Api.PeerStories? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.PeerStories + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.stories.PeerStories.peerStories(stories: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } +} +public extension Api.stories { + enum Stories: TypeConstructorDescription { + case stories(flags: Int32, count: Int32, stories: [Api.StoryItem], pinnedToTop: [Int32]?, chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .stories(let flags, let count, let stories, let pinnedToTop, let chats, let users): + if boxed { + buffer.appendInt32(1673780490) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(stories.count)) + for item in stories { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(pinnedToTop!.count)) + for item in pinnedToTop! { + serializeInt32(item, buffer: buffer, boxed: false) + }} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .stories(let flags, let count, let stories, let pinnedToTop, let chats, let users): + return ("stories", [("flags", flags as Any), ("count", count as Any), ("stories", stories as Any), ("pinnedToTop", pinnedToTop as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_stories(_ reader: BufferReader) -> Stories? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.StoryItem]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryItem.self) + } + var _4: [Int32]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } } + var _5: [Api.Chat]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _6: [Api.User]? + if let _ = reader.readInt32() { + _6 = 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.stories.Stories.stories(flags: _1!, count: _2!, stories: _3!, pinnedToTop: _4, chats: _5!, users: _6!) + } + else { + return nil + } + } + + } +} public extension Api.stories { enum StoryReactionsList: TypeConstructorDescription { case storyReactionsList(flags: Int32, count: Int32, reactions: [Api.StoryReaction], chats: [Api.Chat], users: [Api.User], nextOffset: String?) diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index e2e23ca711e..6c886c37356 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -2200,6 +2200,23 @@ public extension Api.functions.auth { }) } } +public extension Api.functions.bots { + static func addPreviewMedia(bot: Api.InputUser, langCode: String, media: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(397326170) + bot.serialize(buffer, true) + serializeString(langCode, buffer: buffer, boxed: false) + media.serialize(buffer, true) + return (FunctionDescription(name: "bots.addPreviewMedia", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.BotPreviewMedia? in + let reader = BufferReader(buffer) + var result: Api.BotPreviewMedia? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.BotPreviewMedia + } + return result + }) + } +} public extension Api.functions.bots { static func allowSendMessage(bot: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -2246,6 +2263,45 @@ public extension Api.functions.bots { }) } } +public extension Api.functions.bots { + static func deletePreviewMedia(bot: Api.InputUser, langCode: String, media: [Api.InputMedia]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(755054003) + bot.serialize(buffer, true) + serializeString(langCode, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(media.count)) + for item in media { + item.serialize(buffer, true) + } + return (FunctionDescription(name: "bots.deletePreviewMedia", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("media", String(describing: media))]), 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.bots { + static func editPreviewMedia(bot: Api.InputUser, langCode: String, media: Api.InputMedia, newMedia: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-2061148049) + bot.serialize(buffer, true) + serializeString(langCode, buffer: buffer, boxed: false) + media.serialize(buffer, true) + newMedia.serialize(buffer, true) + return (FunctionDescription(name: "bots.editPreviewMedia", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("media", String(describing: media)), ("newMedia", String(describing: newMedia))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.BotPreviewMedia? in + let reader = BufferReader(buffer) + var result: Api.BotPreviewMedia? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.BotPreviewMedia + } + return result + }) + } +} public extension Api.functions.bots { static func getBotCommands(scope: Api.BotCommandScope, langCode: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.BotCommand]>) { let buffer = Buffer() @@ -2294,6 +2350,53 @@ public extension Api.functions.bots { }) } } +public extension Api.functions.bots { + static func getPopularAppBots(offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1034878574) + serializeString(offset, buffer: buffer, boxed: false) + serializeInt32(limit, buffer: buffer, boxed: false) + return (FunctionDescription(name: "bots.getPopularAppBots", parameters: [("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.bots.PopularAppBots? in + let reader = BufferReader(buffer) + var result: Api.bots.PopularAppBots? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.bots.PopularAppBots + } + return result + }) + } +} +public extension Api.functions.bots { + static func getPreviewInfo(bot: Api.InputUser, langCode: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1111143341) + bot.serialize(buffer, true) + serializeString(langCode, buffer: buffer, boxed: false) + return (FunctionDescription(name: "bots.getPreviewInfo", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.bots.PreviewInfo? in + let reader = BufferReader(buffer) + var result: Api.bots.PreviewInfo? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.bots.PreviewInfo + } + return result + }) + } +} +public extension Api.functions.bots { + static func getPreviewMedias(bot: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.BotPreviewMedia]>) { + let buffer = Buffer() + buffer.appendInt32(-1566222003) + bot.serialize(buffer, true) + return (FunctionDescription(name: "bots.getPreviewMedias", parameters: [("bot", String(describing: bot))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.BotPreviewMedia]? in + let reader = BufferReader(buffer) + var result: [Api.BotPreviewMedia]? + if let _ = reader.readInt32() { + result = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotPreviewMedia.self) + } + return result + }) + } +} public extension Api.functions.bots { static func invokeWebViewCustomMethod(bot: Api.InputUser, customMethod: String, params: Api.DataJSON) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -2311,6 +2414,27 @@ public extension Api.functions.bots { }) } } +public extension Api.functions.bots { + static func reorderPreviewMedias(bot: Api.InputUser, langCode: String, order: [Api.InputMedia]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1238895702) + bot.serialize(buffer, true) + serializeString(langCode, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(order.count)) + for item in order { + item.serialize(buffer, true) + } + return (FunctionDescription(name: "bots.reorderPreviewMedias", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("order", String(describing: order))]), 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.bots { static func reorderUsernames(bot: Api.InputUser, order: [String]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7344,6 +7468,26 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func requestMainWebView(flags: Int32, peer: Api.InputPeer, bot: Api.InputUser, startParam: String?, themeParams: Api.DataJSON?, platform: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-908059013) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + bot.serialize(buffer, true) + if Int(flags) & Int(1 << 1) != 0 {serializeString(startParam!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 0) != 0 {themeParams!.serialize(buffer, true)} + serializeString(platform, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.requestMainWebView", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("bot", String(describing: bot)), ("startParam", String(describing: startParam)), ("themeParams", String(describing: themeParams)), ("platform", String(describing: platform))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.WebViewResult? in + let reader = BufferReader(buffer) + var result: Api.WebViewResult? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.WebViewResult + } + return result + }) + } +} public extension Api.functions.messages { static func requestSimpleWebView(flags: Int32, bot: Api.InputUser, url: String?, startParam: String?, themeParams: Api.DataJSON?, platform: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8731,6 +8875,22 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func getStarsGiftOptions(flags: Int32, userId: Api.InputUser?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.StarsGiftOption]>) { + let buffer = Buffer() + buffer.appendInt32(-741774392) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {userId!.serialize(buffer, true)} + return (FunctionDescription(name: "payments.getStarsGiftOptions", parameters: [("flags", String(describing: flags)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.StarsGiftOption]? in + let reader = BufferReader(buffer) + var result: [Api.StarsGiftOption]? + if let _ = reader.readInt32() { + result = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsGiftOption.self) + } + return result + }) + } +} public extension Api.functions.payments { static func getStarsRevenueAdsAccountUrl(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramApi/Sources/Api5.swift b/submodules/TelegramApi/Sources/Api5.swift index b8ae2246383..bd121a4a0b6 100644 --- a/submodules/TelegramApi/Sources/Api5.swift +++ b/submodules/TelegramApi/Sources/Api5.swift @@ -1473,7 +1473,7 @@ public extension Api { case documentAttributeHasStickers case documentAttributeImageSize(w: Int32, h: Int32) case documentAttributeSticker(flags: Int32, alt: String, stickerset: Api.InputStickerSet, maskCoords: Api.MaskCoords?) - case documentAttributeVideo(flags: Int32, duration: Double, w: Int32, h: Int32, preloadPrefixSize: Int32?) + case documentAttributeVideo(flags: Int32, duration: Double, w: Int32, h: Int32, preloadPrefixSize: Int32?, videoStartTs: Double?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -1529,15 +1529,16 @@ public extension Api { stickerset.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {maskCoords!.serialize(buffer, true)} break - case .documentAttributeVideo(let flags, let duration, let w, let h, let preloadPrefixSize): + case .documentAttributeVideo(let flags, let duration, let w, let h, let preloadPrefixSize, let videoStartTs): if boxed { - buffer.appendInt32(-745541182) + buffer.appendInt32(389652397) } serializeInt32(flags, buffer: buffer, boxed: false) serializeDouble(duration, buffer: buffer, boxed: false) serializeInt32(w, buffer: buffer, boxed: false) serializeInt32(h, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 2) != 0 {serializeInt32(preloadPrefixSize!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {serializeDouble(videoStartTs!, buffer: buffer, boxed: false)} break } } @@ -1558,8 +1559,8 @@ public extension Api { return ("documentAttributeImageSize", [("w", w as Any), ("h", h as Any)]) case .documentAttributeSticker(let flags, let alt, let stickerset, let maskCoords): return ("documentAttributeSticker", [("flags", flags as Any), ("alt", alt as Any), ("stickerset", stickerset as Any), ("maskCoords", maskCoords as Any)]) - case .documentAttributeVideo(let flags, let duration, let w, let h, let preloadPrefixSize): - return ("documentAttributeVideo", [("flags", flags as Any), ("duration", duration as Any), ("w", w as Any), ("h", h as Any), ("preloadPrefixSize", preloadPrefixSize as Any)]) + case .documentAttributeVideo(let flags, let duration, let w, let h, let preloadPrefixSize, let videoStartTs): + return ("documentAttributeVideo", [("flags", flags as Any), ("duration", duration as Any), ("w", w as Any), ("h", h as Any), ("preloadPrefixSize", preloadPrefixSize as Any), ("videoStartTs", videoStartTs as Any)]) } } @@ -1671,13 +1672,16 @@ public extension Api { _4 = reader.readInt32() var _5: Int32? if Int(_1!) & Int(1 << 2) != 0 {_5 = reader.readInt32() } + var _6: Double? + if Int(_1!) & Int(1 << 4) != 0 {_6 = reader.readDouble() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.DocumentAttribute.documentAttributeVideo(flags: _1!, duration: _2!, w: _3!, h: _4!, preloadPrefixSize: _5) + let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.DocumentAttribute.documentAttributeVideo(flags: _1!, duration: _2!, w: _3!, h: _4!, preloadPrefixSize: _5, videoStartTs: _6) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api9.swift b/submodules/TelegramApi/Sources/Api9.swift index 32c3e40b12e..4ef5bacfdd3 100644 --- a/submodules/TelegramApi/Sources/Api9.swift +++ b/submodules/TelegramApi/Sources/Api9.swift @@ -412,6 +412,7 @@ public extension Api { enum InputFile: TypeConstructorDescription { case inputFile(id: Int64, parts: Int32, name: String, md5Checksum: String) case inputFileBig(id: Int64, parts: Int32, name: String) + case inputFileStoryDocument(id: Api.InputDocument) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -432,6 +433,12 @@ public extension Api { serializeInt32(parts, buffer: buffer, boxed: false) serializeString(name, buffer: buffer, boxed: false) break + case .inputFileStoryDocument(let id): + if boxed { + buffer.appendInt32(1658620744) + } + id.serialize(buffer, true) + break } } @@ -441,6 +448,8 @@ public extension Api { return ("inputFile", [("id", id as Any), ("parts", parts as Any), ("name", name as Any), ("md5Checksum", md5Checksum as Any)]) case .inputFileBig(let id, let parts, let name): return ("inputFileBig", [("id", id as Any), ("parts", parts as Any), ("name", name as Any)]) + case .inputFileStoryDocument(let id): + return ("inputFileStoryDocument", [("id", id as Any)]) } } @@ -481,6 +490,19 @@ public extension Api { return nil } } + public static func parse_inputFileStoryDocument(_ reader: BufferReader) -> InputFile? { + var _1: Api.InputDocument? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputDocument + } + let _c1 = _1 != nil + if _c1 { + return Api.InputFile.inputFileStoryDocument(id: _1!) + } + else { + return nil + } + } } } @@ -1002,115 +1024,3 @@ public extension Api { } } -public extension Api { - indirect enum InputInvoice: TypeConstructorDescription { - case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32) - case inputInvoicePremiumGiftCode(purpose: Api.InputStorePaymentPurpose, option: Api.PremiumGiftCodeOption) - case inputInvoiceSlug(slug: String) - case inputInvoiceStars(option: Api.StarsTopupOption) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputInvoiceMessage(let peer, let msgId): - if boxed { - buffer.appendInt32(-977967015) - } - peer.serialize(buffer, true) - serializeInt32(msgId, buffer: buffer, boxed: false) - break - case .inputInvoicePremiumGiftCode(let purpose, let option): - if boxed { - buffer.appendInt32(-1734841331) - } - purpose.serialize(buffer, true) - option.serialize(buffer, true) - break - case .inputInvoiceSlug(let slug): - if boxed { - buffer.appendInt32(-1020867857) - } - serializeString(slug, buffer: buffer, boxed: false) - break - case .inputInvoiceStars(let option): - if boxed { - buffer.appendInt32(497236696) - } - option.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputInvoiceMessage(let peer, let msgId): - return ("inputInvoiceMessage", [("peer", peer as Any), ("msgId", msgId as Any)]) - case .inputInvoicePremiumGiftCode(let purpose, let option): - return ("inputInvoicePremiumGiftCode", [("purpose", purpose as Any), ("option", option as Any)]) - case .inputInvoiceSlug(let slug): - return ("inputInvoiceSlug", [("slug", slug as Any)]) - case .inputInvoiceStars(let option): - return ("inputInvoiceStars", [("option", option as Any)]) - } - } - - public static func parse_inputInvoiceMessage(_ reader: BufferReader) -> InputInvoice? { - var _1: Api.InputPeer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputPeer - } - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputInvoice.inputInvoiceMessage(peer: _1!, msgId: _2!) - } - else { - return nil - } - } - public static func parse_inputInvoicePremiumGiftCode(_ reader: BufferReader) -> InputInvoice? { - var _1: Api.InputStorePaymentPurpose? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputStorePaymentPurpose - } - var _2: Api.PremiumGiftCodeOption? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.PremiumGiftCodeOption - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputInvoice.inputInvoicePremiumGiftCode(purpose: _1!, option: _2!) - } - else { - return nil - } - } - public static func parse_inputInvoiceSlug(_ reader: BufferReader) -> InputInvoice? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.InputInvoice.inputInvoiceSlug(slug: _1!) - } - else { - return nil - } - } - public static func parse_inputInvoiceStars(_ reader: BufferReader) -> InputInvoice? { - var _1: Api.StarsTopupOption? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StarsTopupOption - } - let _c1 = _1 != nil - if _c1 { - return Api.InputInvoice.inputInvoiceStars(option: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 49069c9aef6..43e3c87f3ab 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -829,7 +829,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { strongSelf.displayNode.view.window?.endEditing(true) strongSelf.present(controller, in: .window(.root)) } else if case let .messages(chatLocation, _, _) = playlistLocation { - let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tag: .tag(MessageTags.music)) + let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tag: .tag(MessageTags.music)) var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 35877a0bd4f..0bdfd93fb15 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -75,9 +75,9 @@ public final class MediaStreamComponent: CombinedComponent { var videoStalled: Bool = true var videoIsPlayable: Bool { - !videoStalled && hasVideo + return true + //!videoStalled && hasVideo } -// var wantsPiP: Bool = false let deactivatePictureInPictureIfVisible = StoredActionSlot(Void.self) diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift index 2debd4072b7..e14bdfa6a86 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift @@ -6,11 +6,11 @@ import AVKit import MultilineTextComponent import Display import ShimmerEffect - import TelegramCore import SwiftSignalKit import AvatarNode import Postbox +import TelegramVoip final class MediaStreamVideoComponent: Component { let call: PresentationGroupCallImpl @@ -157,6 +157,8 @@ final class MediaStreamVideoComponent: Component { private var lastPresentation: UIView? private var pipTrackDisplayLink: CADisplayLink? + private var livePlayerView: ProxyVideoView? + override init(frame: CGRect) { self.blurTintView = UIView() self.blurTintView.backgroundColor = UIColor(white: 0.0, alpha: 0.55) @@ -211,7 +213,7 @@ final class MediaStreamVideoComponent: Component { let needsFadeInAnimation = hadVideo if loadingBlurView.superview == nil { - addSubview(loadingBlurView) + //addSubview(loadingBlurView) if needsFadeInAnimation { let anim = CABasicAnimation(keyPath: "opacity") anim.duration = 0.5 @@ -542,6 +544,39 @@ final class MediaStreamVideoComponent: Component { videoFrameUpdateTransition.setFrame(layer: self.videoBlurGradientMask, frame: videoBlurView.bounds) videoFrameUpdateTransition.setFrame(layer: self.videoBlurSolidMask, frame: self.videoBlurGradientMask.bounds) } + + if self.livePlayerView == nil { + let livePlayerView = ProxyVideoView(context: component.call.accountContext, call: component.call) + self.livePlayerView = livePlayerView + livePlayerView.layer.masksToBounds = true + self.addSubview(livePlayerView) + livePlayerView.frame = newVideoFrame + livePlayerView.layer.cornerRadius = videoCornerRadius + livePlayerView.update(size: newVideoFrame.size) + + var pictureInPictureController: AVPictureInPictureController? = nil + if #available(iOS 15.0, *) { + pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(playerLayer: livePlayerView.playerLayer)) + pictureInPictureController?.playerLayer.masksToBounds = false + pictureInPictureController?.playerLayer.cornerRadius = 10 + } else if AVPictureInPictureController.isPictureInPictureSupported() { + pictureInPictureController = AVPictureInPictureController.init(playerLayer: AVPlayerLayer(player: AVPlayer())) + } + + pictureInPictureController?.delegate = self + if #available(iOS 14.2, *) { + pictureInPictureController?.canStartPictureInPictureAutomaticallyFromInline = true + } + if #available(iOS 14.0, *) { + pictureInPictureController?.requiresLinearPlayback = true + } + self.pictureInPictureController = pictureInPictureController + } + if let livePlayerView = self.livePlayerView { + videoFrameUpdateTransition.setFrame(view: livePlayerView, frame: newVideoFrame, completion: nil) + videoFrameUpdateTransition.setCornerRadius(layer: livePlayerView.layer, cornerRadius: videoCornerRadius) + livePlayerView.update(size: newVideoFrame.size) + } } else { videoSize = CGSize(width: 16 / 9 * 100.0, height: 100.0).aspectFitted(.init(width: availableSize.width - videoInset * 2, height: availableSize.height)) } @@ -601,7 +636,7 @@ final class MediaStreamVideoComponent: Component { } } - if self.noSignalTimeout { + if self.noSignalTimeout, !"".isEmpty { var noSignalTransition = transition let noSignalView: ComponentHostView if let current = self.noSignalView { @@ -769,3 +804,94 @@ private final class CustomIntensityVisualEffectView: UIVisualEffectView { animator.stopAnimation(true) } } + +private final class ProxyVideoView: UIView { + private let call: PresentationGroupCallImpl + private let id: Int64 + private let player: AVPlayer + private let playerItem: AVPlayerItem + let playerLayer: AVPlayerLayer + + private var contextDisposable: Disposable? + + private var failureObserverId: AnyObject? + private var errorObserverId: AnyObject? + private var rateObserver: NSKeyValueObservation? + + private var isActiveDisposable: Disposable? + + init(context: AccountContext, call: PresentationGroupCallImpl) { + self.call = call + + self.id = Int64.random(in: Int64.min ... Int64.max) + + let assetUrl = "http://127.0.0.1:\(SharedHLSServer.shared.port)/\(call.internalId)/master.m3u8" + Logger.shared.log("MediaStreamVideoComponent", "Initializing HLS asset at \(assetUrl)") + #if DEBUG + print("Initializing HLS asset at \(assetUrl)") + #endif + let asset = AVURLAsset(url: URL(string: assetUrl)!, options: [:]) + self.playerItem = AVPlayerItem(asset: asset) + self.player = AVPlayer(playerItem: self.playerItem) + self.player.allowsExternalPlayback = true + self.playerLayer = AVPlayerLayer(player: self.player) + + super.init(frame: CGRect()) + + self.failureObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.failedToPlayToEndTimeNotification, object: playerItem, queue: .main, using: { notification in + print("Player Error: \(notification.description)") + }) + self.errorObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.newErrorLogEntryNotification, object: playerItem, queue: .main, using: { notification in + print("Player Error: \(notification.description)") + }) + self.rateObserver = self.player.observe(\.rate, changeHandler: { [weak self] _, change in + guard let self else { + return + } + print("Player rate: \(self.player.rate)") + }) + + self.layer.addSublayer(self.playerLayer) + + self.isActiveDisposable = (context.sharedContext.applicationBindings.applicationIsActive + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] isActive in + guard let self else { + return + } + if isActive { + self.playerLayer.player = self.player + if self.player.rate == 0.0 { + self.player.play() + } + } else { + self.playerLayer.player = nil + } + }) + + self.player.play() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.contextDisposable?.dispose() + if let failureObserverId = self.failureObserverId { + NotificationCenter.default.removeObserver(failureObserverId) + } + if let errorObserverId = self.errorObserverId { + NotificationCenter.default.removeObserver(errorObserverId) + } + if let rateObserver = self.rateObserver { + rateObserver.invalidate() + } + self.isActiveDisposable?.dispose() + } + + func update(size: CGSize) { + self.playerLayer.frame = CGRect(origin: CGPoint(), size: size) + } +} + diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 284f939f915..811d2553ef8 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -276,6 +276,7 @@ private extension PresentationGroupCallState { private enum CurrentImpl { case call(OngoingGroupCallContext) case mediaStream(WrappedMediaStreamingContext) + case externalMediaStream(ExternalMediaStreamingContext) } private extension CurrentImpl { @@ -283,7 +284,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): return callContext.joinPayload - case .mediaStream: + case .mediaStream, .externalMediaStream: let ssrcId = UInt32.random(in: 0 ..< UInt32(Int32.max - 1)) let dict: [String: Any] = [ "fingerprints": [] as [Any], @@ -303,7 +304,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): return callContext.networkState - case .mediaStream: + case .mediaStream, .externalMediaStream: return .single(OngoingGroupCallContext.NetworkState(isConnected: true, isTransitioningFromBroadcastToRtc: false)) } } @@ -312,7 +313,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): return callContext.audioLevels - case .mediaStream: + case .mediaStream, .externalMediaStream: return .single([]) } } @@ -321,7 +322,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): return callContext.isMuted - case .mediaStream: + case .mediaStream, .externalMediaStream: return .single(true) } } @@ -330,7 +331,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): return callContext.isNoiseSuppressionEnabled - case .mediaStream: + case .mediaStream, .externalMediaStream: return .single(false) } } @@ -339,7 +340,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.stop() - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -348,7 +349,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.setIsMuted(isMuted) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -357,7 +358,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.setIsNoiseSuppressionEnabled(isNoiseSuppressionEnabled) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -366,7 +367,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.requestVideo(capturer) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -375,7 +376,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.disableVideo() - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -384,7 +385,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.setVolume(ssrc: ssrc, volume: volume) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -393,7 +394,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.setRequestedVideoChannels(channels) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -402,17 +403,19 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.makeIncomingVideoView(endpointId: endpointId, requestClone: requestClone, completion: completion) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } - func video(endpointId: String) -> Signal { + func video(endpointId: String) -> Signal? { switch self { case let .call(callContext): return callContext.video(endpointId: endpointId) case let .mediaStream(mediaStreamContext): return mediaStreamContext.video() + case .externalMediaStream: + return .never() } } @@ -420,7 +423,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.addExternalAudioData(data: data) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -429,7 +432,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.getStats(completion: completion) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -438,7 +441,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.setTone(tone: tone) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -647,6 +650,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private var genericCallContext: CurrentImpl? private var currentConnectionMode: OngoingGroupCallContext.ConnectionMode = .none private var didInitializeConnectionMode: Bool = false + + let externalMediaStream = Promise() private var screencastCallContext: OngoingGroupCallContext? private var screencastBufferServerContext: IpcGroupCallBufferAppContext? @@ -1638,7 +1643,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { genericCallContext = current } else { if self.isStream, self.accountContext.sharedContext.immediateExperimentalUISettings.liveStreamV2 { - genericCallContext = .mediaStream(WrappedMediaStreamingContext(rejoinNeeded: { [weak self] in + let externalMediaStream = ExternalMediaStreamingContext(id: self.internalId, rejoinNeeded: { [weak self] in Queue.mainQueue().async { guard let strongSelf = self else { return @@ -1650,7 +1655,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { strongSelf.requestCall(movingFromBroadcastToRtc: false) } } - })) + }) + genericCallContext = .externalMediaStream(externalMediaStream) + self.externalMediaStream.set(.single(externalMediaStream)) } else { var outgoingAudioBitrateKbit: Int32? let appConfiguration = self.accountContext.currentAppConfiguration.with({ $0 }) @@ -1797,6 +1804,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { strongSelf.currentConnectionMode = .broadcast mediaStreamContext.setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: strongSelf.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash, isExternalStream: callInfo.isStream)) } + case let .externalMediaStream(externalMediaStream): + switch joinCallResult.connectionMode { + case .rtc: + strongSelf.currentConnectionMode = .rtc + case .broadcast: + strongSelf.currentConnectionMode = .broadcast + externalMediaStream.setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: strongSelf.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash, isExternalStream: callInfo.isStream)) + } } } @@ -3199,7 +3214,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { switch genericCallContext { case let .call(callContext): callContext.setConnectionMode(.none, keepBroadcastConnectedIfWasEnabled: movingFromBroadcastToRtc, isUnifiedBroadcast: false) - case .mediaStream: + case .mediaStream, .externalMediaStream: assertionFailure() break } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index dc9a008717c..b4a2ac72395 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -2627,8 +2627,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController if !isScheduled && canSpeak { if #available(iOS 15.0, *) { - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Microphone Modes", textColor: .primary, icon: { theme in + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MicrophoneModes, textColor: .primary, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.dismissWithoutContent) diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 7163cb19947..ce0867744c2 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -542,7 +542,7 @@ struct AccountMutableState { var presences: [PeerId: Api.UserStatus] = [:] for user in users { switch user { - case let .user(_, _, id, _, _, _, _, _, _, status, _, _, _, _, _, _, _, _, _): + case let .user(_, _, id, _, _, _, _, _, _, status, _, _, _, _, _, _, _, _, _, _): if let status = status { presences[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))] = status } diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index afdc774ef56..859695f17a4 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -50,7 +50,7 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute], var isAnimated = false inner: for attribute in file.attributes { switch attribute { - case let .Video(_, _, flags, _): + case let .Video(_, _, flags, _, _): if flags.contains(.instantRoundVideo) { refinedTag = .voiceOrInstantVideo } else { @@ -227,7 +227,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } switch action { - case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe: + case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe: break case let .messageActionChannelMigrateFrom(_, chatId): result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))) @@ -258,6 +258,8 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { if let boostPeer = boostPeer { result.append(boostPeer.peerId) } + case let .messageActionPaymentRefunded(_, peer, _, _, _, _): + result.append(peer.peerId) } return result @@ -520,6 +522,8 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { return .link(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), url: url) case let .mediaAreaChannelPost(coordinates, channelId, messageId): return .channelMessage(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), messageId: EngineMessage.Id(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId)) + case let .mediaAreaWeather(coordinates, emoji, temperatureC, color): + return .weather(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), emoji: emoji, temperature: temperatureC, color: color) } } @@ -572,6 +576,8 @@ func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transac } case let .link(_, url): apiMediaAreas.append(.mediaAreaUrl(coordinates: inputCoordinates, url: url)) + case let .weather(_, emoji, temperature, color): + apiMediaAreas.append(.mediaAreaWeather(coordinates: inputCoordinates, emoji: emoji, temperatureC: temperature, color: color)) } } return apiMediaAreas diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index f29a5c916a1..b4ffe2ba57a 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -102,6 +102,8 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .webViewData(text)) case let .messageActionGiftPremium(_, currency, amount, months, cryptoCurrency, cryptoAmount): return TelegramMediaAction(action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount)) + case let .messageActionGiftStars(_, currency, amount, stars, cryptoCurrency, cryptoAmount, transactionId): + return TelegramMediaAction(action: .giftStars(currency: currency, amount: amount, count: stars, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, transactionId: transactionId)) case let .messageActionTopicCreate(_, title, iconColor, iconEmojiId): return TelegramMediaAction(action: .topicCreated(title: title, iconColor: iconColor, iconFileId: iconEmojiId)) case let .messageActionTopicEdit(flags, title, iconEmojiId, closed, hidden): @@ -139,6 +141,13 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .giveawayResults(winners: winners, unclaimed: unclaimed)) case let .messageActionBoostApply(boosts): return TelegramMediaAction(action: .boostsApplied(boosts: boosts)) + case let .messageActionPaymentRefunded(_, peer, currency, totalAmount, payload, charge): + let transactionId: String + switch charge { + case let .paymentCharge(id, _): + transactionId = id + } + return TelegramMediaAction(action: .paymentRefunded(peerId: peer.peerId, currency: currency, totalAmount: totalAmount, payload: payload?.makeData(), transactionId: transactionId)) } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift index 9daed9bc012..611db7cedc4 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift @@ -6,7 +6,7 @@ import TelegramApi func dimensionsForFileAttributes(_ attributes: [TelegramMediaFileAttribute]) -> PixelDimensions? { for attribute in attributes { switch attribute { - case let .Video(_, size, _, _): + case let .Video(_, size, _, _, _): return size case let .ImageSize(size): return size @@ -20,7 +20,7 @@ func dimensionsForFileAttributes(_ attributes: [TelegramMediaFileAttribute]) -> func durationForFileAttributes(_ attributes: [TelegramMediaFileAttribute]) -> Double? { for attribute in attributes { switch attribute { - case let .Video(duration, _, _, _): + case let .Video(duration, _, _, _, _): return duration case let .Audio(_, duration, _, _, _): return Double(duration) @@ -99,7 +99,7 @@ func telegramMediaFileAttributesFromApiAttributes(_ attributes: [Api.DocumentAtt result.append(.ImageSize(size: PixelDimensions(width: w, height: h))) case .documentAttributeAnimated: result.append(.Animated) - case let .documentAttributeVideo(flags, duration, w, h, preloadSize): + case let .documentAttributeVideo(flags, duration, w, h, preloadSize, videoStart): var videoFlags = TelegramMediaVideoFlags() if (flags & (1 << 0)) != 0 { videoFlags.insert(.instantRoundVideo) @@ -110,7 +110,7 @@ func telegramMediaFileAttributesFromApiAttributes(_ attributes: [Api.DocumentAtt if (flags & (1 << 3)) != 0 { videoFlags.insert(.isSilent) } - result.append(.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: preloadSize)) + result.append(.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: preloadSize, coverTime: videoStart)) case let .documentAttributeAudio(flags, duration, title, performer, waveform): let isVoice = (flags & (1 << 10)) != 0 let waveformBuffer: Data? = waveform?.makeData() diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift index e9c38bdd938..23ea3fd5e4b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift @@ -36,7 +36,7 @@ extension TelegramPeerUsername { extension TelegramUser { convenience init(user: Api.User) { switch user { - case let .user(flags, flags2, id, accessHash, firstName, lastName, username, phone, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, usernames, _, color, profileColor): + case let .user(flags, flags2, id, accessHash, firstName, lastName, username, phone, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, usernames, _, color, profileColor, subscriberCount): let representations: [TelegramMediaImageRepresentation] = photo.flatMap(parsedTelegramProfilePhoto) ?? [] let isMin = (flags & (1 << 20)) != 0 @@ -99,6 +99,9 @@ extension TelegramUser { if (flags2 & (1 << 11)) != 0 { botFlags.insert(.isBusiness) } + if (flags2 & (1 << 13)) != 0 { + botFlags.insert(.hasWebApp) + } botInfo = BotUserInfo(flags: botFlags, inlinePlaceholder: botInlinePlaceholder) } @@ -124,15 +127,15 @@ extension TelegramUser { } } - self.init(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)), accessHash: accessHashValue, firstName: firstName, lastName: lastName, username: username, phone: phone, photo: representations, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId) + self.init(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)), accessHash: accessHashValue, firstName: firstName, lastName: lastName, username: username, phone: phone, photo: representations, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, subscriberCount: subscriberCount) case let .userEmpty(id): - self.init(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)), 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) + self.init(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)), 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) } } static func merge(_ lhs: TelegramUser?, rhs: Api.User) -> TelegramUser? { switch rhs { - case let .user(flags, _, _, rhsAccessHash, _, _, _, _, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, _, _, nameColor, profileColor): + case let .user(flags, _, _, rhsAccessHash, _, _, _, _, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, _, _, nameColor, profileColor, subscriberCount): let isMin = (flags & (1 << 20)) != 0 if !isMin { return TelegramUser(user: rhs) @@ -233,7 +236,7 @@ extension TelegramUser { } } - return TelegramUser(id: lhs.id, accessHash: accessHash, firstName: lhs.firstName, lastName: lhs.lastName, username: lhs.username, phone: lhs.phone, photo: telegramPhoto, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), usernames: lhs.usernames, storiesHidden: lhs.storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId) + return TelegramUser(id: lhs.id, accessHash: accessHash, firstName: lhs.firstName, lastName: lhs.lastName, username: lhs.username, phone: lhs.phone, photo: telegramPhoto, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), usernames: lhs.usernames, storiesHidden: lhs.storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, subscriberCount: subscriberCount) } else { return TelegramUser(user: rhs) } @@ -294,7 +297,7 @@ extension TelegramUser { storiesHidden = lhs.storiesHidden } - return TelegramUser(id: lhs.id, accessHash: accessHash, firstName: lhs.firstName, lastName: lhs.lastName, username: lhs.username, phone: lhs.phone, photo: photo, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags, emojiStatus: emojiStatus, usernames: lhs.usernames, storiesHidden: storiesHidden, nameColor: rhs.nameColor, backgroundEmojiId: rhs.backgroundEmojiId, profileColor: rhs.profileColor, profileBackgroundEmojiId: rhs.profileBackgroundEmojiId) + return TelegramUser(id: lhs.id, accessHash: accessHash, firstName: lhs.firstName, lastName: lhs.lastName, username: lhs.username, phone: lhs.phone, photo: photo, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags, emojiStatus: emojiStatus, usernames: lhs.usernames, storiesHidden: storiesHidden, nameColor: rhs.nameColor, backgroundEmojiId: rhs.backgroundEmojiId, profileColor: rhs.profileColor, profileBackgroundEmojiId: rhs.profileBackgroundEmojiId, subscriberCount: rhs.subscriberCount) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramUserPresence.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUserPresence.swift index df313c16c16..ff0a3f19003 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramUserPresence.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramUserPresence.swift @@ -26,7 +26,7 @@ extension TelegramUserPresence { convenience init?(apiUser: Api.User) { switch apiUser { - case let .user(_, _, _, _, _, _, _, _, _, status, _, _, _, _, _, _, _, _, _): + case let .user(_, _, _, _, _, _, _, _, _, status, _, _, _, _, _, _, _, _, _, _): if let status = status { self.init(apiStatus: status) } else { diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index 00f94cf8840..e01f8261185 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -316,6 +316,7 @@ private enum MediaReferenceRevalidationKey: Hashable { case notificationSoundList case customEmoji(fileId: Int64) case story(peer: PeerReference, id: Int32) + case starsTransaction(transaction: StarsTransactionReference) } private final class MediaReferenceRevalidationItemContext { @@ -752,6 +753,30 @@ final class MediaReferenceRevalidationContext { } } + func starsTransaction(accountPeerId: PeerId, postbox: Postbox, network: Network, background: Bool, transaction: StarsTransactionReference) -> Signal { + return self.genericItem(key: .starsTransaction(transaction: transaction), background: background, request: { next, error in + return (_internal_getStarsTransaction(accountPeerId: accountPeerId, postbox: postbox, network: network, transactionReference: transaction) + |> castError(RevalidateMediaReferenceError.self) + |> mapToSignal { result -> Signal in + if let result { + return .single(result) + } else { + return .fail(.generic) + } + }).start(next: { value in + next(value) + }, error: { _ in + error(.generic) + }) + }) |> mapToSignal { next -> Signal in + if let next = next as? StarsContext.State.Transaction { + return .single(next) + } else { + return .fail(.generic) + } + } + } + func notificationSoundList(postbox: Postbox, network: Network, background: Bool) -> Signal<[TelegramMediaFile], RevalidateMediaReferenceError> { return self.genericItem(key: .notificationSoundList, background: background, request: { next, error in return (requestNotificationSoundList(network: network, hash: 0) @@ -937,6 +962,16 @@ func revalidateMediaResourceReference(accountPeerId: PeerId, postbox: Postbox, n return .fail(.generic) } } + case let .starsTransaction(transaction, _): + return revalidationContext.starsTransaction(accountPeerId: accountPeerId, postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation, transaction: transaction) + |> mapToSignal { transaction -> Signal in + for transactionMedia in transaction.media { + if let updatedResource = findUpdatedMediaResource(media: transactionMedia, previousMedia: nil, resource: resource) { + return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)) + } + } + return .fail(.generic) + } case let .standalone(media): if let file = media as? TelegramMediaFile { for attribute in file.attributes { diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index f76bc6d1851..6d3872c4926 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -701,7 +701,7 @@ func inputDocumentAttributesFromFileAttributes(_ fileAttributes: [TelegramMediaF attributes.append(.documentAttributeSticker(flags: flags, alt: displayText, stickerset: stickerSet, maskCoords: inputMaskCoords)) case .HasLinkedStickers: attributes.append(.documentAttributeHasStickers) - case let .Video(duration, size, videoFlags, preloadSize): + case let .Video(duration, size, videoFlags, preloadSize, coverTime): var flags: Int32 = 0 if videoFlags.contains(.instantRoundVideo) { flags |= (1 << 0) @@ -715,8 +715,10 @@ func inputDocumentAttributesFromFileAttributes(_ fileAttributes: [TelegramMediaF if videoFlags.contains(.isSilent) { flags |= (1 << 3) } - - attributes.append(.documentAttributeVideo(flags: flags, duration: duration, w: Int32(size.width), h: Int32(size.height), preloadPrefixSize: preloadSize)) + if let coverTime = coverTime, coverTime > 0.0 { + flags |= (1 << 4) + } + attributes.append(.documentAttributeVideo(flags: flags, duration: duration, w: Int32(size.width), h: Int32(size.height), preloadPrefixSize: preloadSize, videoStartTs: coverTime)) case let .Audio(isVoice, duration, title, performer, waveform): var flags: Int32 = 0 if isVoice { @@ -786,7 +788,7 @@ public func statsCategoryForFileWithAttributes(_ attributes: [TelegramMediaFileA } else { return .audio } - case let .Video(_, _, flags, _): + case let .Video(_, _, flags, _, _): if flags.contains(TelegramMediaVideoFlags.instantRoundVideo) { return .voiceMessages } else { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 5e00964182a..7e67b1668b5 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1121,7 +1121,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if updatedState.peers[peerId] == nil { updatedState.updatePeer(peerId, { peer in if peer == nil { - return TelegramUser(id: peerId, accessHash: nil, firstName: "Telegram Notifications", lastName: nil, username: nil, phone: nil, photo: [], botInfo: BotUserInfo(flags: [], inlinePlaceholder: nil), restrictionInfo: nil, flags: [.isVerified], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + return TelegramUser(id: peerId, accessHash: nil, firstName: "Telegram Notifications", lastName: nil, username: nil, phone: nil, photo: [], botInfo: BotUserInfo(flags: [], inlinePlaceholder: nil), restrictionInfo: nil, flags: [.isVerified], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) } else { return peer } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index 9fa6beed64b..f2f7f5321f0 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -287,6 +287,11 @@ public final class AccountStateManager { return self.storyUpdatesPipe.signal() } + fileprivate let botPreviewUpdatesPipe = ValuePipe<[InternalBotPreviewUpdate]>() + public var botPreviewUpdates: Signal<[InternalBotPreviewUpdate], NoError> { + return self.botPreviewUpdatesPipe.signal() + } + private var updatedWebpageContexts: [MediaId: UpdatedWebpageSubscriberContext] = [:] private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext() private var updatedRevenueBalancesContext = UpdatedRevenueBalancesSubscriberContext() @@ -1856,6 +1861,18 @@ public final class AccountStateManager { } } + var botPreviewUpdates: Signal<[InternalBotPreviewUpdate], NoError> { + return self.impl.signalWith { impl, subscriber in + return impl.botPreviewUpdates.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) + } + } + + func injectBotPreviewUpdates(updates: [InternalBotPreviewUpdate]) { + self.impl.with { impl in + impl.botPreviewUpdatesPipe.putNext(updates) + } + } + var updateConfigRequested: (() -> Void)? var isPremiumUpdated: (() -> Void)? diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 432ed9bf486..15d15fb5791 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -1472,7 +1472,7 @@ public final class AccountViewTracker { if i < slice.count { let value = result[i] transaction.updatePeerCachedData(peerIds: Set([slice[i].0]), update: { _, cachedData in - var cachedData = cachedData as? CachedUserData ?? CachedUserData(about: nil, botInfo: nil, editableBotInfo: nil, peerStatusSettings: nil, pinnedMessageId: nil, isBlocked: false, commonGroupCount: 0, voiceCallsAvailable: true, videoCallsAvailable: true, callsPrivate: true, canPinMessages: true, hasScheduledMessages: true, autoremoveTimeout: .unknown, themeEmoticon: nil, photo: .unknown, personalPhoto: .unknown, fallbackPhoto: .unknown, premiumGiftOptions: [], voiceMessagesAvailable: true, wallpaper: nil, flags: [], businessHours: nil, businessLocation: nil, greetingMessage: nil, awayMessage: nil, connectedBot: nil, businessIntro: .unknown, birthday: nil, personalChannel: .unknown) + var cachedData = cachedData as? CachedUserData ?? CachedUserData(about: nil, botInfo: nil, editableBotInfo: nil, peerStatusSettings: nil, pinnedMessageId: nil, isBlocked: false, commonGroupCount: 0, voiceCallsAvailable: true, videoCallsAvailable: true, callsPrivate: true, canPinMessages: true, hasScheduledMessages: true, autoremoveTimeout: .unknown, themeEmoticon: nil, photo: .unknown, personalPhoto: .unknown, fallbackPhoto: .unknown, premiumGiftOptions: [], voiceMessagesAvailable: true, wallpaper: nil, flags: [], businessHours: nil, businessLocation: nil, greetingMessage: nil, awayMessage: nil, connectedBot: nil, businessIntro: .unknown, birthday: nil, personalChannel: .unknown, botPreview: nil) var flags = cachedData.flags if case .boolTrue = value { flags.insert(.premiumRequired) diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index edf64213f0d..be44796999f 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -14,7 +14,7 @@ private func copyOrMoveResourceData(from fromResource: MediaResource, to toResou } } -func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force: Bool) { +func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force: Bool, skipPreviews: Bool = false) { if let fromImage = from as? TelegramMediaImage, let toImage = to as? TelegramMediaImage { let fromSmallestRepresentation = smallestImageRepresentation(fromImage.representations) if let fromSmallestRepresentation = fromSmallestRepresentation, let toSmallestRepresentation = smallestImageRepresentation(toImage.representations) { @@ -32,11 +32,13 @@ func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force: } } } else if let fromFile = from as? TelegramMediaFile, let toFile = to as? TelegramMediaFile { - if let fromPreview = smallestImageRepresentation(fromFile.previewRepresentations), let toPreview = smallestImageRepresentation(toFile.previewRepresentations) { - copyOrMoveResourceData(from: fromPreview.resource, to: toPreview.resource, mediaBox: postbox.mediaBox) - } - if let fromVideoThumbnail = fromFile.videoThumbnails.first, let toVideoThumbnail = toFile.videoThumbnails.first, fromVideoThumbnail.resource.id != toVideoThumbnail.resource.id { - copyOrMoveResourceData(from: fromVideoThumbnail.resource, to: toVideoThumbnail.resource, mediaBox: postbox.mediaBox) + if !skipPreviews { + if let fromPreview = smallestImageRepresentation(fromFile.previewRepresentations), let toPreview = smallestImageRepresentation(toFile.previewRepresentations) { + copyOrMoveResourceData(from: fromPreview.resource, to: toPreview.resource, mediaBox: postbox.mediaBox) + } + if let fromVideoThumbnail = fromFile.videoThumbnails.first, let toVideoThumbnail = toFile.videoThumbnails.first, fromVideoThumbnail.resource.id != toVideoThumbnail.resource.id { + copyOrMoveResourceData(from: fromVideoThumbnail.resource, to: toVideoThumbnail.resource, mediaBox: postbox.mediaBox) + } } let videoFirstFrameFromPath = postbox.mediaBox.cachedRepresentationCompletePath(fromFile.resource.id, keepDuration: .general, representationId: "first-frame") let videoFirstFrameToPath = postbox.mediaBox.cachedRepresentationCompletePath(toFile.resource.id, keepDuration: .general, representationId: "first-frame") diff --git a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift index 20b9c662dd2..88374c2a7e6 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift @@ -553,7 +553,7 @@ private func decryptedAttributes46(_ attributes: [TelegramMediaFileAttribute], t result.append(.documentAttributeSticker(alt: displayText, stickerset: stickerSet)) case let .ImageSize(size): result.append(.documentAttributeImageSize(w: Int32(size.width), h: Int32(size.height))) - case let .Video(duration, size, _, _): + case let .Video(duration, size, _, _, _): result.append(.documentAttributeVideo(duration: Int32(duration), w: Int32(size.width), h: Int32(size.height))) case let .Audio(isVoice, duration, title, performer, waveform): var flags: Int32 = 0 @@ -612,7 +612,7 @@ private func decryptedAttributes73(_ attributes: [TelegramMediaFileAttribute], t result.append(.documentAttributeSticker(alt: displayText, stickerset: stickerSet)) case let .ImageSize(size): result.append(.documentAttributeImageSize(w: Int32(size.width), h: Int32(size.height))) - case let .Video(duration, size, videoFlags, _): + case let .Video(duration, size, videoFlags, _, _): var flags: Int32 = 0 if videoFlags.contains(.instantRoundVideo) { flags |= 1 << 0 @@ -675,7 +675,7 @@ private func decryptedAttributes101(_ attributes: [TelegramMediaFileAttribute], result.append(.documentAttributeSticker(alt: displayText, stickerset: stickerSet)) case let .ImageSize(size): result.append(.documentAttributeImageSize(w: Int32(size.width), h: Int32(size.height))) - case let .Video(duration, size, videoFlags, _): + case let .Video(duration, size, videoFlags, _, _): var flags: Int32 = 0 if videoFlags.contains(.instantRoundVideo) { flags |= 1 << 0 @@ -738,7 +738,7 @@ private func decryptedAttributes144(_ attributes: [TelegramMediaFileAttribute], result.append(.documentAttributeSticker(alt: displayText, stickerset: stickerSet)) case let .ImageSize(size): result.append(.documentAttributeImageSize(w: Int32(size.width), h: Int32(size.height))) - case let .Video(duration, size, videoFlags, _): + case let .Video(duration, size, videoFlags, _, _): var flags: Int32 = 0 if videoFlags.contains(.instantRoundVideo) { flags |= 1 << 0 diff --git a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift index 24085d51105..ddf283a43d4 100644 --- a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift +++ b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift @@ -610,7 +610,7 @@ extension TelegramMediaFileAttribute { } self = .Sticker(displayText: alt, packReference: packReference, maskData: nil) case let .documentAttributeVideo(duration, w, h): - self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil) + self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil) } } } @@ -642,7 +642,7 @@ extension TelegramMediaFileAttribute { if (flags & (1 << 0)) != 0 { videoFlags.insert(.instantRoundVideo) } - self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil) + self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil, coverTime: nil) } } } @@ -674,7 +674,7 @@ extension TelegramMediaFileAttribute { if (flags & (1 << 0)) != 0 { videoFlags.insert(.instantRoundVideo) } - self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil) + self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil, coverTime: nil) } } } @@ -706,7 +706,7 @@ extension TelegramMediaFileAttribute { if (flags & (1 << 0)) != 0 { videoFlags.insert(.instantRoundVideo) } - self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil) + self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil, coverTime: nil) } } } @@ -821,7 +821,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 text = caption } if let file = file { - let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil), .FileName(fileName: "video.mov")] + let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil), .FileName(fileName: "video.mov")] var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) @@ -1021,7 +1021,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 loop: for attr in parsedAttributes { switch attr { - case let .Video(_, _, flags, _): + case let .Video(_, _, flags, _, _): if flags.contains(.instantRoundVideo) { attributes.append(ConsumableContentMessageAttribute(consumed: false)) } @@ -1040,7 +1040,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 text = caption } if let file = file { - let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil), .FileName(fileName: "video.mov")] + let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil), .FileName(fileName: "video.mov")] var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) @@ -1300,7 +1300,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 loop: for attr in parsedAttributes { switch attr { - case let .Video(_, _, flags, _): + case let .Video(_, _, flags, _, _): if flags.contains(.instantRoundVideo) { attributes.append(ConsumableContentMessageAttribute(consumed: false)) } @@ -1319,7 +1319,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 text = caption } if let file = file { - let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil), .FileName(fileName: "video.mov")] + let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil), .FileName(fileName: "video.mov")] var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) @@ -1501,7 +1501,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 loop: for attr in parsedAttributes { switch attr { - case let .Video(_, _, flags, _): + case let .Video(_, _, flags, _, _): if flags.contains(.instantRoundVideo) { attributes.append(ConsumableContentMessageAttribute(consumed: false)) } @@ -1520,7 +1520,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 text = caption } if let file = file { - let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil), .FileName(fileName: "video.mov")] + let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil), .FileName(fileName: "video.mov")] var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 4e852ebc47e..9bbff0d1df7 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 183 + return 185 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index f938a9ea431..0ec47fcdf35 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -193,7 +193,7 @@ extension Api.Chat { extension Api.User { var peerId: PeerId { switch self { - case let .user(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .user(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)) case let .userEmpty(id): return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)) diff --git a/submodules/TelegramCore/Sources/Statistics/PeerStatistics.swift b/submodules/TelegramCore/Sources/Statistics/PeerStatistics.swift index c0a080c0568..8bb3a41beea 100644 --- a/submodules/TelegramCore/Sources/Statistics/PeerStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/PeerStatistics.swift @@ -19,7 +19,21 @@ public struct StatsPercentValue: Equatable { public let total: Double } -public enum StatsGraph: Equatable { +public enum StatsGraph: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case type + case token + case error + case data + } + + private enum TypeKey: Int32 { + case onDemand + case failed + case loaded + case empty + } + case OnDemand(token: String) case Failed(error: String) case Loaded(token: String?, data: String) @@ -46,6 +60,41 @@ public enum StatsGraph: Equatable { return nil } } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = TypeKey(rawValue: try container.decode(Int32.self, forKey: .type)) ?? .empty + switch type { + case .onDemand: + self = .OnDemand(token: try container.decode(String.self, forKey: .token)) + case .failed: + self = .Failed(error: try container.decode(String.self, forKey: .error)) + case .loaded: + self = .Loaded(token: try container.decodeIfPresent(String.self, forKey: .token), data: try container.decode(String.self, forKey: .data)) + case .empty: + self = .Empty + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case let .OnDemand(token): + try container.encode(TypeKey.empty.rawValue, forKey: .type) + try container.encode(token, forKey: .token) + case let .Failed(error): + try container.encode(TypeKey.failed.rawValue, forKey: .type) + try container.encode(error, forKey: .error) + case let .Loaded(token, data): + try container.encode(TypeKey.loaded.rawValue, forKey: .type) + try container.encodeIfPresent(token, forKey: .token) + try container.encode(data, forKey: .data) + case .Empty: + try container.encode(TypeKey.empty.rawValue, forKey: .type) + } + } } public struct ChannelStatsPostInteractions: Equatable { diff --git a/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift b/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift index 31edbae2b21..784164b5432 100644 --- a/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift @@ -4,11 +4,54 @@ import Postbox import TelegramApi import MtProtoKit -public struct RevenueStats: Equatable { - public struct Balances: Equatable { +public struct RevenueStats: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case topHoursGraph + case revenueGraph + case balances + case usdRate + } + + static func key(peerId: PeerId) -> ValueBoxKey { + let key = ValueBoxKey(length: 8 + 4) + key.setInt64(0, value: peerId.toInt64()) + return key + } + + public struct Balances: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case currentBalance + case availableBalance + case overallRevenue + } + public let currentBalance: Int64 public let availableBalance: Int64 public let overallRevenue: Int64 + + init( + currentBalance: Int64, + availableBalance: Int64, + overallRevenue: Int64 + ) { + self.currentBalance = currentBalance + self.availableBalance = availableBalance + self.overallRevenue = overallRevenue + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.currentBalance = try container.decode(Int64.self, forKey: .currentBalance) + self.availableBalance = try container.decode(Int64.self, forKey: .availableBalance) + self.overallRevenue = try container.decode(Int64.self, forKey: .overallRevenue) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.currentBalance, forKey: .currentBalance) + try container.encode(self.availableBalance, forKey: .availableBalance) + try container.encode(self.overallRevenue, forKey: .overallRevenue) + } } public let topHoursGraph: StatsGraph @@ -23,6 +66,22 @@ public struct RevenueStats: Equatable { self.usdRate = usdRate } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.topHoursGraph = try container.decode(StatsGraph.self, forKey: .topHoursGraph) + self.revenueGraph = try container.decode(StatsGraph.self, forKey: .revenueGraph) + self.balances = try container.decode(Balances.self, forKey: .balances) + self.usdRate = try container.decode(Double.self, forKey: .usdRate) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.topHoursGraph, forKey: .topHoursGraph) + try container.encode(self.revenueGraph, forKey: .revenueGraph) + try container.encode(self.balances, forKey: .balances) + try container.encode(self.usdRate, forKey: .usdRate) + } + public static func == (lhs: RevenueStats, rhs: RevenueStats) -> Bool { if lhs.topHoursGraph != rhs.topHoursGraph { return false @@ -124,6 +183,17 @@ private final class RevenueStatsContextImpl { self._statePromise.set(.single(self._state)) self.load() + + let _ = (account.postbox.transaction { transaction -> RevenueStats? in + return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedRevenueStats, key: StarsRevenueStats.key(peerId: peerId)))?.get(RevenueStats.self) + } + |> deliverOnMainQueue).start(next: { [weak self] cachedResult in + guard let self, let cachedResult else { + return + } + self._state = RevenueStatsContextState(stats: cachedResult) + self._statePromise.set(.single(self._state)) + }) } deinit { @@ -155,9 +225,17 @@ private final class RevenueStatsContextImpl { self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] stats in - if let strongSelf = self { - strongSelf._state = RevenueStatsContextState(stats: stats) - strongSelf._statePromise.set(.single(strongSelf._state)) + if let self { + self._state = RevenueStatsContextState(stats: stats) + self._statePromise.set(.single(self._state)) + + if let stats { + let _ = (self.account.postbox.transaction { transaction in + if let entry = CodableEntry(stats) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedRevenueStats, key: StarsRevenueStats.key(peerId: peerId)), entry: entry) + } + }).start() + } } })) } diff --git a/submodules/TelegramCore/Sources/Statistics/StarsRevenueStatistics.swift b/submodules/TelegramCore/Sources/Statistics/StarsRevenueStatistics.swift index 09cf65e4c7b..eb192715701 100644 --- a/submodules/TelegramCore/Sources/Statistics/StarsRevenueStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/StarsRevenueStatistics.swift @@ -4,13 +4,65 @@ import Postbox import TelegramApi import MtProtoKit -public struct StarsRevenueStats: Equatable { - public struct Balances: Equatable { +public struct StarsRevenueStats: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case revenueGraph + case balances + case usdRate + } + + static func key(peerId: PeerId) -> ValueBoxKey { + let key = ValueBoxKey(length: 8 + 4) + key.setInt64(0, value: peerId.toInt64()) + return key + } + + public struct Balances: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case currentBalance + case availableBalance + case overallRevenue + case withdrawEnabled + case nextWithdrawalTimestamp + } + public let currentBalance: Int64 public let availableBalance: Int64 public let overallRevenue: Int64 public let withdrawEnabled: Bool public let nextWithdrawalTimestamp: Int32? + + public init( + currentBalance: Int64, + availableBalance: Int64, + overallRevenue: Int64, + withdrawEnabled: Bool, + nextWithdrawalTimestamp: Int32? + ) { + self.currentBalance = currentBalance + self.availableBalance = availableBalance + self.overallRevenue = overallRevenue + self.withdrawEnabled = withdrawEnabled + self.nextWithdrawalTimestamp = nextWithdrawalTimestamp + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.currentBalance = try container.decode(Int64.self, forKey: .currentBalance) + self.availableBalance = try container.decode(Int64.self, forKey: .availableBalance) + self.overallRevenue = try container.decode(Int64.self, forKey: .overallRevenue) + self.withdrawEnabled = try container.decode(Bool.self, forKey: .withdrawEnabled) + self.nextWithdrawalTimestamp = try container.decodeIfPresent(Int32.self, forKey: .nextWithdrawalTimestamp) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.currentBalance, forKey: .currentBalance) + try container.encode(self.availableBalance, forKey: .availableBalance) + try container.encode(self.overallRevenue, forKey: .overallRevenue) + try container.encode(self.withdrawEnabled, forKey: .withdrawEnabled) + try container.encodeIfPresent(self.nextWithdrawalTimestamp, forKey: .nextWithdrawalTimestamp) + } } public let revenueGraph: StatsGraph @@ -23,6 +75,20 @@ public struct StarsRevenueStats: Equatable { self.usdRate = usdRate } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.revenueGraph = try container.decode(StatsGraph.self, forKey: .revenueGraph) + self.balances = try container.decode(Balances.self, forKey: .balances) + self.usdRate = try container.decode(Double.self, forKey: .usdRate) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.revenueGraph, forKey: .revenueGraph) + try container.encode(self.balances, forKey: .balances) + try container.encode(self.usdRate, forKey: .usdRate) + } + public static func == (lhs: StarsRevenueStats, rhs: StarsRevenueStats) -> Bool { if lhs.revenueGraph != rhs.revenueGraph { return false @@ -121,6 +187,17 @@ private final class StarsRevenueStatsContextImpl { self._statePromise.set(.single(self._state)) self.load() + + let _ = (account.postbox.transaction { transaction -> StarsRevenueStats? in + return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStarsRevenueStats, key: StarsRevenueStats.key(peerId: peerId)))?.get(StarsRevenueStats.self) + } + |> deliverOnMainQueue).start(next: { [weak self] cachedResult in + guard let self, let cachedResult else { + return + } + self._state = StarsRevenueStatsContextState(stats: cachedResult) + self._statePromise.set(.single(self._state)) + }) } deinit { @@ -163,9 +240,17 @@ private final class StarsRevenueStatsContextImpl { self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] stats in - if let strongSelf = self { - strongSelf._state = StarsRevenueStatsContextState(stats: stats) - strongSelf._statePromise.set(.single(strongSelf._state)) + if let self { + self._state = StarsRevenueStatsContextState(stats: stats) + self._statePromise.set(.single(self._state)) + + if let stats { + let _ = (self.account.postbox.transaction { transaction in + if let entry = CodableEntry(stats) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStarsRevenueStats, key: StarsRevenueStats.key(peerId: peerId)), entry: entry) + } + }).start() + } } })) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift index 8ca472a617e..e3e2bd689b0 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift @@ -23,6 +23,7 @@ public struct CachedChannelFlags: OptionSet { public static let adsRestricted = CachedChannelFlags(rawValue: 1 << 9) public static let canViewRevenue = CachedChannelFlags(rawValue: 1 << 10) public static let paidMediaAllowed = CachedChannelFlags(rawValue: 1 << 11) + public static let canViewStarsRevenue = CachedChannelFlags(rawValue: 1 << 12) } public struct CachedChannelParticipantsSummary: PostboxCoding, Equatable { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index ea394efd5d8..f35daeb38a2 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -625,6 +625,95 @@ extension TelegramBusinessChatLinks { } public final class CachedUserData: CachedPeerData { + public final class BotPreview: Codable, Equatable { + private enum CodingKeys: String, CodingKey { + case items + case alternativeLanguageCodes + } + + public final class Item: Codable, Equatable { + private enum CodingKeys: String, CodingKey { + case media = "m" + case timestamp = "t" + } + + public let media: Media + public let timestamp: Int32 + + public init(media: Media, timestamp: Int32) { + self.media = media + self.timestamp = timestamp + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let mediaData = try container.decode(Data.self, forKey: .media) + guard let media = PostboxDecoder(buffer: MemoryBuffer(data: mediaData)).decodeRootObject() as? Media else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "media")) + } + self.media = media + + self.timestamp = try container.decode(Int32.self, forKey: .timestamp) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + let encoder = PostboxEncoder() + encoder.encodeRootObject(media) + try container.encode(encoder.makeData(), forKey: .media) + + try container.encode(self.timestamp, forKey: .timestamp) + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs === rhs { + return true + } + if !lhs.media.isEqual(to: rhs.media) { + return false + } + return true + } + } + + public let items: [Item] + public let alternativeLanguageCodes: [String] + + public init(items: [Item], alternativeLanguageCodes: [String]) { + self.items = items + self.alternativeLanguageCodes = alternativeLanguageCodes + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.items = try container.decode([Item].self, forKey: .items) + self.alternativeLanguageCodes = try container.decode([String].self, forKey: .alternativeLanguageCodes) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.items, forKey: .items) + try container.encode(self.alternativeLanguageCodes, forKey: .alternativeLanguageCodes) + } + + public static func ==(lhs: BotPreview, rhs: BotPreview) -> Bool { + if lhs === rhs { + return true + } + if lhs.items != rhs.items { + return false + } + if lhs.alternativeLanguageCodes != rhs.alternativeLanguageCodes { + return false + } + return true + } + } + public let about: String? public let botInfo: BotInfo? public let editableBotInfo: EditableBotInfo? @@ -654,6 +743,7 @@ public final class CachedUserData: CachedPeerData { public let businessIntro: CachedTelegramBusinessIntro public let birthday: TelegramBirthday? public let personalChannel: CachedTelegramPersonalChannel + public let botPreview: BotPreview? public let peerIds: Set public let messageIds: Set @@ -691,9 +781,10 @@ public final class CachedUserData: CachedPeerData { self.businessIntro = .unknown self.birthday = nil self.personalChannel = .unknown + self.botPreview = nil } - public init(about: String?, botInfo: BotInfo?, editableBotInfo: EditableBotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, voiceCallsAvailable: Bool, videoCallsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool, autoremoveTimeout: CachedPeerAutoremoveTimeout, themeEmoticon: String?, photo: CachedPeerProfilePhoto, personalPhoto: CachedPeerProfilePhoto, fallbackPhoto: CachedPeerProfilePhoto, premiumGiftOptions: [CachedPremiumGiftOption], voiceMessagesAvailable: Bool, wallpaper: TelegramWallpaper?, flags: CachedUserFlags, businessHours: TelegramBusinessHours?, businessLocation: TelegramBusinessLocation?, greetingMessage: TelegramBusinessGreetingMessage?, awayMessage: TelegramBusinessAwayMessage?, connectedBot: TelegramAccountConnectedBot?, businessIntro: CachedTelegramBusinessIntro, birthday: TelegramBirthday?, personalChannel: CachedTelegramPersonalChannel) { + public init(about: String?, botInfo: BotInfo?, editableBotInfo: EditableBotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, voiceCallsAvailable: Bool, videoCallsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool, autoremoveTimeout: CachedPeerAutoremoveTimeout, themeEmoticon: String?, photo: CachedPeerProfilePhoto, personalPhoto: CachedPeerProfilePhoto, fallbackPhoto: CachedPeerProfilePhoto, premiumGiftOptions: [CachedPremiumGiftOption], voiceMessagesAvailable: Bool, wallpaper: TelegramWallpaper?, flags: CachedUserFlags, businessHours: TelegramBusinessHours?, businessLocation: TelegramBusinessLocation?, greetingMessage: TelegramBusinessGreetingMessage?, awayMessage: TelegramBusinessAwayMessage?, connectedBot: TelegramAccountConnectedBot?, businessIntro: CachedTelegramBusinessIntro, birthday: TelegramBirthday?, personalChannel: CachedTelegramPersonalChannel, botPreview: BotPreview?) { self.about = about self.botInfo = botInfo self.editableBotInfo = editableBotInfo @@ -723,6 +814,7 @@ public final class CachedUserData: CachedPeerData { self.businessIntro = businessIntro self.birthday = birthday self.personalChannel = personalChannel + self.botPreview = botPreview self.peerIds = Set() @@ -786,6 +878,8 @@ public final class CachedUserData: CachedPeerData { self.birthday = decoder.decodeCodable(TelegramBirthday.self, forKey: "bday") self.personalChannel = decoder.decodeCodable(CachedTelegramPersonalChannel.self, forKey: "pchan") ?? .unknown + + self.botPreview = decoder.decodeCodable(BotPreview.self, forKey: "botPreview") } public func encode(_ encoder: PostboxEncoder) { @@ -885,7 +979,12 @@ public final class CachedUserData: CachedPeerData { encoder.encodeNil(forKey: "bday") } - encoder.encodeCodable(personalChannel, forKey: "pchan") + encoder.encodeCodable(self.personalChannel, forKey: "pchan") + if let botPreview = self.botPreview { + encoder.encodeCodable(botPreview, forKey: "botPreview") + } else { + encoder.encodeNil(forKey: "botPreview") + } } public func isEqual(to: CachedPeerData) -> Bool { @@ -923,124 +1022,131 @@ public final class CachedUserData: CachedPeerData { if other.personalChannel != self.personalChannel { return false } + if other.botPreview != self.botPreview { + return false + } return other.about == self.about && other.botInfo == self.botInfo && other.editableBotInfo == self.editableBotInfo && self.peerStatusSettings == other.peerStatusSettings && self.isBlocked == other.isBlocked && self.commonGroupCount == other.commonGroupCount && self.voiceCallsAvailable == other.voiceCallsAvailable && self.videoCallsAvailable == other.videoCallsAvailable && self.callsPrivate == other.callsPrivate && self.hasScheduledMessages == other.hasScheduledMessages && self.autoremoveTimeout == other.autoremoveTimeout && self.themeEmoticon == other.themeEmoticon && self.photo == other.photo && self.personalPhoto == other.personalPhoto && self.fallbackPhoto == other.fallbackPhoto && self.premiumGiftOptions == other.premiumGiftOptions && self.voiceMessagesAvailable == other.voiceMessagesAvailable && self.flags == other.flags && self.wallpaper == other.wallpaper } public func withUpdatedAbout(_ about: String?) -> CachedUserData { - return CachedUserData(about: about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedBotInfo(_ botInfo: BotInfo?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedEditableBotInfo(_ editableBotInfo: EditableBotInfo?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedIsBlocked(_ isBlocked: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedCommonGroupCount(_ commonGroupCount: Int32) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedVoiceCallsAvailable(_ voiceCallsAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedVideoCallsAvailable(_ videoCallsAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedCallsPrivate(_ callsPrivate: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedCanPinMessages(_ canPinMessages: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedAutoremoveTimeout(_ autoremoveTimeout: CachedPeerAutoremoveTimeout) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedThemeEmoticon(_ themeEmoticon: String?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedPhoto(_ photo: CachedPeerProfilePhoto) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedPersonalPhoto(_ personalPhoto: CachedPeerProfilePhoto) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedFallbackPhoto(_ fallbackPhoto: CachedPeerProfilePhoto) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedPremiumGiftOptions(_ premiumGiftOptions: [CachedPremiumGiftOption]) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedVoiceMessagesAvailable(_ voiceMessagesAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedWallpaper(_ wallpaper: TelegramWallpaper?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedFlags(_ flags: CachedUserFlags) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedBusinessHours(_ businessHours: TelegramBusinessHours?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedBusinessLocation(_ businessLocation: TelegramBusinessLocation?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedGreetingMessage(_ greetingMessage: TelegramBusinessGreetingMessage?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedAwayMessage(_ awayMessage: TelegramBusinessAwayMessage?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedConnectedBot(_ connectedBot: TelegramAccountConnectedBot?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedBusinessIntro(_ businessIntro: TelegramBusinessIntro?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: .known(businessIntro), birthday: self.birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: .known(businessIntro), birthday: self.birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedBirthday(_ birthday: TelegramBirthday?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: birthday, personalChannel: self.personalChannel) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: birthday, personalChannel: self.personalChannel, botPreview: self.botPreview) } public func withUpdatedPersonalChannel(_ personalChannel: TelegramPersonalChannel?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: .known(personalChannel)) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: .known(personalChannel), botPreview: self.botPreview) + } + + public func withUpdatedBotPreview(_ botPreview: BotPreview?) -> CachedUserData { + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel, botPreview: botPreview) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift index afe09615209..64adab4a100 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift @@ -268,6 +268,7 @@ public enum AnyMediaReference: Equatable { case attachBot(peer: PeerReference, media: Media) case customEmoji(media: Media) case story(peer: PeerReference, id: Int32, media: Media) + case starsTransaction(transaction: StarsTransactionReference, media: Media) public static func ==(lhs: AnyMediaReference, rhs: AnyMediaReference) -> Bool { switch lhs { @@ -337,6 +338,12 @@ public enum AnyMediaReference: Equatable { } else { return false } + case let .starsTransaction(lhsTransaction, lhsMedia): + if case let .starsTransaction(rhsTransaction, rhsMedia) = rhs, lhsTransaction == rhsTransaction, lhsMedia.isEqual(to: rhsMedia) { + return true + } else { + return false + } } } @@ -364,6 +371,8 @@ public enum AnyMediaReference: Equatable { return nil case .story: return nil + case .starsTransaction: + return nil } } @@ -413,6 +422,10 @@ public enum AnyMediaReference: Equatable { if let media = media as? T { return .story(peer: peer, id: id, media: media) } + case let .starsTransaction(transaction, media): + if let media = media as? T { + return .starsTransaction(transaction: transaction, media: media) + } } return nil } @@ -441,6 +454,8 @@ public enum AnyMediaReference: Equatable { return media case let .story(_, _, media): return media + case let .starsTransaction(_, media): + return media } } @@ -468,6 +483,8 @@ public enum AnyMediaReference: Equatable { return .customEmoji(media: media) case let .story(peer, id, _): return .story(peer: peer, id: id, media: media) + case let .starsTransaction(transaction, _): + return .starsTransaction(transaction: transaction, media: media) } } @@ -567,6 +584,7 @@ public enum MediaReference { case attachBot case customEmoji case story + case starsTransaction } case standalone(media: T) @@ -580,6 +598,7 @@ public enum MediaReference { case attachBot(peer: PeerReference, media: T) case customEmoji(media: T) case story(peer: PeerReference, id: Int32, media: T) + case starsTransaction(transaction: StarsTransactionReference, media: T) public init?(decoder: PostboxDecoder) { guard let caseIdValue = decoder.decodeOptionalInt32ForKey("_r"), let caseId = CodingCase(rawValue: caseIdValue) else { @@ -648,54 +667,65 @@ public enum MediaReference { } let id = decoder.decodeInt32ForKey("sid", orElse: 0) self = .story(peer: peer, id: id, media: media) + case .starsTransaction: + let transaction = decoder.decodeObjectForKey("tr", decoder: { StarsTransactionReference(decoder: $0) }) as! StarsTransactionReference + guard let media = decoder.decodeObjectForKey("m") as? T else { + return nil + } + self = .starsTransaction(transaction: transaction, media: media) } } public func encode(_ encoder: PostboxEncoder) { switch self { - case let .standalone(media): - encoder.encodeInt32(CodingCase.standalone.rawValue, forKey: "_r") - encoder.encodeObject(media, forKey: "m") - case let .message(message, media): - encoder.encodeInt32(CodingCase.message.rawValue, forKey: "_r") - encoder.encodeObject(message, forKey: "msg") - encoder.encodeObject(media, forKey: "m") - case let .webPage(webPage, media): - encoder.encodeInt32(CodingCase.webPage.rawValue, forKey: "_r") - encoder.encodeObject(webPage, forKey: "wpg") - encoder.encodeObject(media, forKey: "m") - case let .stickerPack(stickerPack, media): - encoder.encodeInt32(CodingCase.stickerPack.rawValue, forKey: "_r") - encoder.encodeObject(stickerPack, forKey: "spk") - encoder.encodeObject(media, forKey: "m") - case let .savedGif(media): - encoder.encodeInt32(CodingCase.savedGif.rawValue, forKey: "_r") - encoder.encodeObject(media, forKey: "m") - case let .savedSticker(media): - encoder.encodeInt32(CodingCase.savedSticker.rawValue, forKey: "_r") - encoder.encodeObject(media, forKey: "m") - case let .recentSticker(media): - encoder.encodeInt32(CodingCase.recentSticker.rawValue, forKey: "_r") - encoder.encodeObject(media, forKey: "m") - case let .avatarList(peer, media): - encoder.encodeInt32(CodingCase.avatarList.rawValue, forKey: "_r") - encoder.encodeObject(peer, forKey: "pr") - encoder.encodeObject(media, forKey: "m") - case let .attachBot(peer, media): - encoder.encodeInt32(CodingCase.attachBot.rawValue, forKey: "_r") - encoder.encodeObject(peer, forKey: "pr") - encoder.encodeObject(media, forKey: "m") - case let .customEmoji(media): - encoder.encodeInt32(CodingCase.customEmoji.rawValue, forKey: "_r") - encoder.encodeObject(media, forKey: "m") - case let .story(peer, id, media): - encoder.encodeInt32(CodingCase.story.rawValue, forKey: "_r") - encoder.encodeObject(peer, forKey: "pr") - encoder.encodeInt32(id, forKey: "sid") - encoder.encodeObject(media, forKey: "m") + case let .standalone(media): + encoder.encodeInt32(CodingCase.standalone.rawValue, forKey: "_r") + encoder.encodeObject(media, forKey: "m") + case let .message(message, media): + encoder.encodeInt32(CodingCase.message.rawValue, forKey: "_r") + encoder.encodeObject(message, forKey: "msg") + encoder.encodeObject(media, forKey: "m") + case let .webPage(webPage, media): + encoder.encodeInt32(CodingCase.webPage.rawValue, forKey: "_r") + encoder.encodeObject(webPage, forKey: "wpg") + encoder.encodeObject(media, forKey: "m") + case let .stickerPack(stickerPack, media): + encoder.encodeInt32(CodingCase.stickerPack.rawValue, forKey: "_r") + encoder.encodeObject(stickerPack, forKey: "spk") + encoder.encodeObject(media, forKey: "m") + case let .savedGif(media): + encoder.encodeInt32(CodingCase.savedGif.rawValue, forKey: "_r") + encoder.encodeObject(media, forKey: "m") + case let .savedSticker(media): + encoder.encodeInt32(CodingCase.savedSticker.rawValue, forKey: "_r") + encoder.encodeObject(media, forKey: "m") + case let .recentSticker(media): + encoder.encodeInt32(CodingCase.recentSticker.rawValue, forKey: "_r") + encoder.encodeObject(media, forKey: "m") + case let .avatarList(peer, media): + encoder.encodeInt32(CodingCase.avatarList.rawValue, forKey: "_r") + encoder.encodeObject(peer, forKey: "pr") + encoder.encodeObject(media, forKey: "m") + case let .attachBot(peer, media): + encoder.encodeInt32(CodingCase.attachBot.rawValue, forKey: "_r") + encoder.encodeObject(peer, forKey: "pr") + encoder.encodeObject(media, forKey: "m") + case let .customEmoji(media): + encoder.encodeInt32(CodingCase.customEmoji.rawValue, forKey: "_r") + encoder.encodeObject(media, forKey: "m") + case let .story(peer, id, media): + encoder.encodeInt32(CodingCase.story.rawValue, forKey: "_r") + encoder.encodeObject(peer, forKey: "pr") + encoder.encodeInt32(id, forKey: "sid") + encoder.encodeObject(media, forKey: "m") + case let .starsTransaction(transaction, media): + encoder.encodeInt32(CodingCase.starsTransaction.rawValue, forKey: "_r") + encoder.encodeObject(transaction, forKey: "tr") + encoder.encodeObject(media, forKey: "m") } } + public var abstract: AnyMediaReference { switch self { case let .standalone(media): @@ -720,6 +750,8 @@ public enum MediaReference { return .customEmoji(media: media) case let .story(peer, id, media): return .story(peer: peer, id: id, media: media) + case let .starsTransaction(transaction, media): + return .starsTransaction(transaction: transaction, media: media) } } @@ -751,6 +783,8 @@ public enum MediaReference { return media case let .story(_, _, media): return media + case let .starsTransaction(_, media): + return media } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 4c07120c5ee..928ba32a69f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -14,6 +14,12 @@ public struct Namespaces { public static let allScheduled: Set = Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal]) public static let allQuickReply: Set = Set([Namespaces.Message.QuickReplyCloud, Namespaces.Message.QuickReplyLocal]) public static let allNonRegular: Set = Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal, Namespaces.Message.QuickReplyCloud, Namespaces.Message.QuickReplyLocal]) + public static let allLocal: [Int32] = [ + Namespaces.Message.Local, + Namespaces.Message.SecretIncoming, + Namespaces.Message.ScheduledLocal, + Namespaces.Message.QuickReplyLocal + ] } public struct Media { @@ -126,6 +132,9 @@ public struct Namespaces { public static let savedMessageTags: Int8 = 35 public static let applicationIcons: Int8 = 36 public static let availableMessageEffects: Int8 = 37 + public static let cachedStarsRevenueStats: Int8 = 38 + public static let cachedRevenueStats: Int8 = 39 + public static let recommendedApps: Int8 = 40 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 6dbb501727b..8f6bd1cd658 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -127,6 +127,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case joinedChannel case giveawayResults(winners: Int32, unclaimed: Int32) case boostsApplied(boosts: Int32) + case paymentRefunded(peerId: PeerId, currency: String, totalAmount: Int64, payload: Data?, transactionId: String) + case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -235,6 +237,10 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { self = .giveawayResults(winners: decoder.decodeInt32ForKey("winners", orElse: 0), unclaimed: decoder.decodeInt32ForKey("unclaimed", orElse: 0)) case 40: self = .boostsApplied(boosts: decoder.decodeInt32ForKey("boosts", orElse: 0)) + case 41: + self = .paymentRefunded(peerId: PeerId(decoder.decodeInt64ForKey("pi", orElse: 0)), currency: decoder.decodeStringForKey("currency", orElse: ""), totalAmount: decoder.decodeInt64ForKey("amount", orElse: 0), payload: decoder.decodeDataForKey("payload"), transactionId: decoder.decodeStringForKey("transactionId", orElse: "")) + case 42: + self = .giftStars(currency: decoder.decodeStringForKey("currency", orElse: ""), amount: decoder.decodeInt64ForKey("amount", orElse: 0), count: decoder.decodeInt64ForKey("count", orElse: 0), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"), transactionId: decoder.decodeOptionalStringForKey("transactionId")) default: self = .unknown } @@ -457,6 +463,34 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case let .boostsApplied(boosts): encoder.encodeInt32(40, forKey: "_rawValue") encoder.encodeInt32(boosts, forKey: "boosts") + case let .paymentRefunded(peerId, currency, totalAmount, payload, transactionId): + encoder.encodeInt32(41, forKey: "_rawValue") + encoder.encodeInt64(peerId.toInt64(), forKey: "pi") + encoder.encodeString(currency, forKey: "currency") + encoder.encodeInt64(totalAmount, forKey: "amount") + if let payload { + encoder.encodeData(payload, forKey: "payload") + } else { + encoder.encodeNil(forKey: "payload") + } + encoder.encodeString(transactionId, forKey: "transactionId") + case let .giftStars(currency, amount, count, cryptoCurrency, cryptoAmount, transactionId): + encoder.encodeInt32(42, forKey: "_rawValue") + encoder.encodeString(currency, forKey: "currency") + encoder.encodeInt64(amount, forKey: "amount") + encoder.encodeInt64(count, forKey: "count") + if let cryptoCurrency = cryptoCurrency, let cryptoAmount = cryptoAmount { + encoder.encodeString(cryptoCurrency, forKey: "cryptoCurrency") + encoder.encodeInt64(cryptoAmount, forKey: "cryptoAmount") + } else { + encoder.encodeNil(forKey: "cryptoCurrency") + encoder.encodeNil(forKey: "cryptoAmount") + } + if let transactionId { + encoder.encodeString(transactionId, forKey: "transactionId") + } else { + encoder.encodeNil(forKey: "transactionId") + } } } @@ -480,6 +514,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { return peerIds case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _): return boostPeerId.flatMap { [$0] } ?? [] + case let .paymentRefunded(peerId, _, _, _, _): + return [peerId] default: return [] } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index 1ae8b35fd7d..aaddf2debb2 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -235,7 +235,7 @@ public enum TelegramMediaFileAttribute: PostboxCoding, Equatable { case Sticker(displayText: String, packReference: StickerPackReference?, maskData: StickerMaskCoords?) case ImageSize(size: PixelDimensions) case Animated - case Video(duration: Double, size: PixelDimensions, flags: TelegramMediaVideoFlags, preloadSize: Int32?) + case Video(duration: Double, size: PixelDimensions, flags: TelegramMediaVideoFlags, preloadSize: Int32?, coverTime: Double?) case Audio(isVoice: Bool, duration: Int, title: String?, performer: String?, waveform: Data?) case HasLinkedStickers case hintFileIsLarge @@ -262,7 +262,7 @@ public enum TelegramMediaFileAttribute: PostboxCoding, Equatable { duration = Double(decoder.decodeInt32ForKey("du", orElse: 0)) } - self = .Video(duration: duration, size: PixelDimensions(width: decoder.decodeInt32ForKey("w", orElse: 0), height: decoder.decodeInt32ForKey("h", orElse: 0)), flags: TelegramMediaVideoFlags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0)), preloadSize: decoder.decodeOptionalInt32ForKey("prs")) + self = .Video(duration: duration, size: PixelDimensions(width: decoder.decodeInt32ForKey("w", orElse: 0), height: decoder.decodeInt32ForKey("h", orElse: 0)), flags: TelegramMediaVideoFlags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0)), preloadSize: decoder.decodeOptionalInt32ForKey("prs"), coverTime: decoder.decodeOptionalDoubleForKey("ct")) case typeAudio: let waveformBuffer = decoder.decodeBytesForKeyNoCopy("wf") var waveform: Data? @@ -309,7 +309,7 @@ public enum TelegramMediaFileAttribute: PostboxCoding, Equatable { encoder.encodeInt32(Int32(size.height), forKey: "h") case .Animated: encoder.encodeInt32(typeAnimated, forKey: "t") - case let .Video(duration, size, flags, preloadSize): + case let .Video(duration, size, flags, preloadSize, coverTime): encoder.encodeInt32(typeVideo, forKey: "t") encoder.encodeDouble(duration, forKey: "dur") encoder.encodeInt32(Int32(size.width), forKey: "w") @@ -320,6 +320,11 @@ public enum TelegramMediaFileAttribute: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "prs") } + if let coverTime = coverTime { + encoder.encodeDouble(coverTime, forKey: "ct") + } else { + encoder.encodeNil(forKey: "ct") + } case let .Audio(isVoice, duration, title, performer, waveform): encoder.encodeInt32(typeAudio, forKey: "t") encoder.encodeInt32(isVoice ? 1 : 0, forKey: "iv") @@ -592,7 +597,7 @@ public final class TelegramMediaFile: Media, Equatable, Codable { public var isInstantVideo: Bool { for attribute in self.attributes { - if case .Video(_, _, let flags, _) = attribute { + if case .Video(_, _, let flags, _, _) = attribute { return flags.contains(.instantRoundVideo) } } @@ -601,7 +606,7 @@ public final class TelegramMediaFile: Media, Equatable, Codable { public var preloadSize: Int32? { for attribute in self.attributes { - if case .Video(_, _, _, let preloadSize) = attribute { + if case .Video(_, _, _, let preloadSize, _) = attribute { return preloadSize } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift index d1586a041fc..d5dc9bf17ef 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift @@ -38,6 +38,7 @@ public struct BotUserInfoFlags: OptionSet { public static let canBeAddedToAttachMenu = BotUserInfoFlags(rawValue: (1 << 4)) public static let canEdit = BotUserInfoFlags(rawValue: (1 << 5)) public static let isBusiness = BotUserInfoFlags(rawValue: (1 << 6)) + public static let hasWebApp = BotUserInfoFlags(rawValue: (1 << 7)) } public struct BotUserInfo: PostboxCoding, Equatable { @@ -117,6 +118,7 @@ public final class TelegramUser: Peer, Equatable { public let backgroundEmojiId: Int64? public let profileColor: PeerNameColor? public let profileBackgroundEmojiId: Int64? + public let subscriberCount: Int32? public var nameOrPhone: String { if let firstName = self.firstName { @@ -205,7 +207,8 @@ public final class TelegramUser: Peer, Equatable { nameColor: PeerNameColor?, backgroundEmojiId: Int64?, profileColor: PeerNameColor?, - profileBackgroundEmojiId: Int64? + profileBackgroundEmojiId: Int64?, + subscriberCount: Int32? ) { self.id = id self.accessHash = accessHash @@ -224,6 +227,7 @@ public final class TelegramUser: Peer, Equatable { self.backgroundEmojiId = backgroundEmojiId self.profileColor = profileColor self.profileBackgroundEmojiId = profileBackgroundEmojiId + self.subscriberCount = subscriberCount } public init(decoder: PostboxDecoder) { @@ -268,6 +272,8 @@ public final class TelegramUser: Peer, Equatable { self.backgroundEmojiId = decoder.decodeOptionalInt64ForKey("bgem") self.profileColor = decoder.decodeOptionalInt32ForKey("pclr").flatMap { PeerNameColor(rawValue: $0) } self.profileBackgroundEmojiId = decoder.decodeOptionalInt64ForKey("pgem") + + self.subscriberCount = decoder.decodeOptionalInt32ForKey("ssc") } public func encode(_ encoder: PostboxEncoder) { @@ -351,6 +357,12 @@ public final class TelegramUser: Peer, Equatable { } else { encoder.encodeNil(forKey: "pgem") } + + if let subscriberCount = self.subscriberCount { + encoder.encodeInt32(subscriberCount, forKey: "ssc") + } else { + encoder.encodeNil(forKey: "ssc") + } } public func isEqual(_ other: Peer) -> Bool { @@ -418,55 +430,58 @@ public final class TelegramUser: Peer, Equatable { if lhs.profileBackgroundEmojiId != rhs.profileBackgroundEmojiId { return false } + if lhs.subscriberCount != rhs.subscriberCount { + return false + } return true } public func withUpdatedUsername(_ username: String?) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, subscriberCount: self.subscriberCount) } public func withUpdatedUsernames(_ usernames: [TelegramPeerUsername]) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, subscriberCount: self.subscriberCount) } public func withUpdatedNames(firstName: String?, lastName: String?) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: firstName, lastName: lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: firstName, lastName: lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, subscriberCount: self.subscriberCount) } public func withUpdatedPhone(_ phone: String?) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, subscriberCount: self.subscriberCount) } public func withUpdatedPhoto(_ representations: [TelegramMediaImageRepresentation]) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: representations, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: representations, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, subscriberCount: self.subscriberCount) } public func withUpdatedEmojiStatus(_ emojiStatus: PeerEmojiStatus?) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, subscriberCount: self.subscriberCount) } public func withUpdatedFlags(_ flags: UserInfoFlags) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, subscriberCount: self.subscriberCount) } public func withUpdatedStoriesHidden(_ storiesHidden: Bool?) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, subscriberCount: self.subscriberCount) } public func withUpdatedNameColor(_ nameColor: PeerNameColor) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, subscriberCount: self.subscriberCount) } public func withUpdatedBackgroundEmojiId(_ backgroundEmojiId: Int64?) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, subscriberCount: self.subscriberCount) } public func withUpdatedProfileColor(_ profileColor: PeerNameColor?) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, subscriberCount: self.subscriberCount) } public func withUpdatedProfileBackgroundEmojiId(_ profileBackgroundEmojiId: Int64?) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId, subscriberCount: self.subscriberCount) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 0cef10c2f4a..4c6c5e094dd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -1096,6 +1096,33 @@ public extension TelegramEngine.EngineData.Item { } } + public struct CanViewStarsRevenue: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Bool + + 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? CachedChannelData { + return cachedData.flags.contains(.canViewStarsRevenue) + } else { + return false + } + } + } public struct PaidMediaAllowed: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = Bool @@ -2020,5 +2047,89 @@ public extension TelegramEngine.EngineData.Item { } } } + + public struct BotPreview: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = CachedUserData.BotPreview? + + 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.botPreview + } else { + return nil + } + } + } + + public struct BotMenu: 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?.menuButton + } else { + return nil + } + } + } + + public struct BotCommands: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Optional<[BotCommand]> + + 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?.commands + } else { + return nil + } + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift index 2649a4b3aa9..0123ff375c3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift @@ -592,7 +592,7 @@ func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network |> switchToLatest } -public enum BotAppReference { +public enum BotAppReference : Equatable { case id(id: Int64, accessHash: Int64) case shortName(peerId: PeerId, shortName: String) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index cdbcd868d8c..622ddeac154 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -60,6 +60,50 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P |> switchToLatest } +func _internal_requestMainWebView(postbox: Postbox, network: Network, botId: PeerId, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { + var serializedThemeParams: Api.DataJSON? + if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { + serializedThemeParams = .dataJSON(data: dataString) + } + return postbox.transaction { transaction -> Signal in + guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else { + return .fail(.generic) + } + guard let peer = transaction.getPeer(botId), let inputPeer = apiInputPeer(peer) else { + return .fail(.generic) + } + + var flags: Int32 = 0 + if let _ = serializedThemeParams { + flags |= (1 << 0) + } + switch source { + case .inline: + flags |= (1 << 1) + case .settings: + flags |= (1 << 2) + default: + break + } + return network.request(Api.functions.messages.requestMainWebView(flags: flags, peer: inputPeer, bot: inputUser, startParam: nil, themeParams: serializedThemeParams, platform: botWebViewPlatform)) + |> mapError { _ -> RequestWebViewError in + return .generic + } + |> mapToSignal { result -> Signal in + switch result { + case let .webViewResultUrl(flags, queryId, url): + var resultFlags: RequestWebViewResult.Flags = [] + if (flags & (1 << 1)) != 0 { + resultFlags.insert(.fullSize) + } + return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: nil)) + } + } + } + |> castError(RequestWebViewError.self) + |> switchToLatest +} + public enum KeepWebViewError { case generic } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift index 74d9ae7a78d..dec49da75ab 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift @@ -7,6 +7,8 @@ public enum MediaArea: Codable, Equatable { case coordinates case value case flags + case temperature + case color } public struct Coordinates: Codable, Equatable { @@ -149,6 +151,7 @@ public enum MediaArea: Codable, Equatable { case reaction(coordinates: Coordinates, reaction: MessageReaction.Reaction, flags: ReactionFlags) case channelMessage(coordinates: Coordinates, messageId: EngineMessage.Id) case link(coordinates: Coordinates, url: String) + case weather(coordinates: Coordinates, emoji: String, temperature: Double, color: Int32) public struct ReactionFlags: OptionSet { public var rawValue: Int32 @@ -164,13 +167,13 @@ public enum MediaArea: Codable, Equatable { public static let isDark = ReactionFlags(rawValue: 1 << 0) public static let isFlipped = ReactionFlags(rawValue: 1 << 1) } - private enum MediaAreaType: Int32 { case venue case reaction case channelMessage case link + case weather } public enum DecodingError: Error { @@ -201,6 +204,12 @@ public enum MediaArea: Codable, Equatable { let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates) let url = try container.decode(String.self, forKey: .value) self = .link(coordinates: coordinates, url: url) + case .weather: + let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates) + let emoji = try container.decode(String.self, forKey: .value) + let temperature = try container.decode(Double.self, forKey: .temperature) + let color = try container.decodeIfPresent(Int32.self, forKey: .color) ?? 0 + self = .weather(coordinates: coordinates, emoji: emoji, temperature: temperature, color: color) } } @@ -225,6 +234,12 @@ public enum MediaArea: Codable, Equatable { try container.encode(MediaAreaType.link.rawValue, forKey: .type) try container.encode(coordinates, forKey: .coordinates) try container.encode(url, forKey: .value) + case let .weather(coordinates, emoji, temperature, color): + try container.encode(MediaAreaType.weather.rawValue, forKey: .type) + try container.encode(coordinates, forKey: .coordinates) + try container.encode(emoji, forKey: .value) + try container.encode(temperature, forKey: .temperature) + try container.encode(color, forKey: .color) } } } @@ -240,6 +255,8 @@ public extension MediaArea { return coordinates case let .link(coordinates, _): return coordinates + case let .weather(coordinates, _, _, _): + return coordinates } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift index de9bc76cf89..019afadf9e2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift @@ -118,7 +118,7 @@ func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: if let dimensions = externalReference.content?.dimensions { fileAttributes.append(.ImageSize(size: dimensions)) if externalReference.type == "gif" { - fileAttributes.append(.Video(duration: externalReference.content?.duration ?? 0.0, size: dimensions, flags: [], preloadSize: nil)) + fileAttributes.append(.Video(duration: externalReference.content?.duration ?? 0.0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift index 6c094379ee8..f78a3a6aa6f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift @@ -8,10 +8,12 @@ public extension Stories { private enum CodingKeys: String, CodingKey { case discriminator = "tt" case peerId = "peerId" + case language = "language" } case myStories case peer(PeerId) + case botPreview(id: PeerId, language: String?) public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -21,6 +23,8 @@ public extension Stories { self = .myStories case 1: self = .peer(try container.decode(PeerId.self, forKey: .peerId)) + case 2: + self = .botPreview(id: try container.decode(PeerId.self, forKey: .peerId), language: try container.decodeIfPresent(String.self, forKey: .language)) default: self = .myStories } @@ -35,6 +39,10 @@ public extension Stories { case let .peer(peerId): try container.encode(1 as Int32, forKey: .discriminator) try container.encode(peerId, forKey: .peerId) + case let .botPreview(peerId, language): + try container.encode(2 as Int32, forKey: .discriminator) + try container.encode(peerId, forKey: .peerId) + try container.encodeIfPresent(language, forKey: .language) } } } @@ -369,12 +377,14 @@ final class PendingStoryManager { print(currentPendingItemContext) }) } - self.queuedPendingItems = Set(localState.items.map { item -> PeerId in + self.queuedPendingItems = Set(localState.items.compactMap { item -> PeerId? in switch item.target { case .myStories: return self.accountPeerId case let .peer(id): return id + case .botPreview: + return nil } }) @@ -397,33 +407,78 @@ final class PendingStoryManager { self.currentPendingItemContext = pendingItemContext let toPeerId: PeerId + var isBotPreview = false + var botPreviewLanguage: String? switch firstItem.target { case .myStories: toPeerId = self.accountPeerId case let .peer(peerId): toPeerId = peerId + case let .botPreview(peerId, language): + toPeerId = peerId + botPreviewLanguage = language + isBotPreview = true } let stableId = firstItem.stableId - pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, toPeerId: toPeerId, stableId: stableId, media: firstItem.media, mediaAreas: firstItem.mediaAreas, text: firstItem.text, entities: firstItem.entities, embeddedStickers: firstItem.embeddedStickers, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), randomId: firstItem.randomId, forwardInfo: firstItem.forwardInfo) - |> deliverOn(self.queue)).start(next: { [weak self] event in - guard let `self` = self else { - return - } - switch event { - case let .progress(progress): - if let currentPendingItemContext = self.currentPendingItemContext, currentPendingItemContext.item.stableId == stableId { - currentPendingItemContext.progress = progress - currentPendingItemContext.updated() + if isBotPreview { + pendingItemContext.disposable = (_internal_uploadBotPreviewImpl( + postbox: self.postbox, + network: self.network, + accountPeerId: self.accountPeerId, + stateManager: self.stateManager, + messageMediaPreuploadManager: self.messageMediaPreuploadManager, + revalidationContext: self.revalidationContext, + auxiliaryMethods: self.auxiliaryMethods, + toPeerId: toPeerId, + language: botPreviewLanguage, + stableId: stableId, + media: firstItem.media, + mediaAreas: firstItem.mediaAreas, + text: firstItem.text, + entities: firstItem.entities, + embeddedStickers: firstItem.embeddedStickers, + randomId: firstItem.randomId + ) + |> deliverOn(self.queue)).start(next: { [weak self] event in + guard let self else { + return } - case let .completed(id): - if let id = id { - self.allStoriesEventsPipe.putNext((stableId, id)) + switch event { + case let .progress(progress): + if let currentPendingItemContext = self.currentPendingItemContext, currentPendingItemContext.item.stableId == stableId { + currentPendingItemContext.progress = progress + currentPendingItemContext.updated() + } + case let .completed(id): + if let id = id { + self.allStoriesEventsPipe.putNext((stableId, id)) + } + // wait for the local state to change via Postbox + break } - // wait for the local state to change via Postbox - break - } - }) + }) + } else { + pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, toPeerId: toPeerId, stableId: stableId, media: firstItem.media, mediaAreas: firstItem.mediaAreas, text: firstItem.text, entities: firstItem.entities, embeddedStickers: firstItem.embeddedStickers, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), randomId: firstItem.randomId, forwardInfo: firstItem.forwardInfo) + |> deliverOn(self.queue)).start(next: { [weak self] event in + guard let `self` = self else { + return + } + switch event { + case let .progress(progress): + if let currentPendingItemContext = self.currentPendingItemContext, currentPendingItemContext.item.stableId == stableId { + currentPendingItemContext.progress = progress + currentPendingItemContext.updated() + } + case let .completed(id): + if let id = id { + self.allStoriesEventsPipe.putNext((stableId, id)) + } + // wait for the local state to change via Postbox + break + } + }) + } } self.processContextsUpdated() @@ -440,6 +495,8 @@ final class PendingStoryManager { currentProgress[self.accountPeerId] = currentPendingItemContext.progress case let .peer(id): currentProgress[id] = currentPendingItemContext.progress + case .botPreview: + break } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index 84aedac1f25..baac6eb27b2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift @@ -236,6 +236,59 @@ private func mergedResult(_ state: SearchMessagesState) -> SearchMessagesResult return SearchMessagesResult(messages: messages, readStates: readStates, threadInfo: threadInfo, totalCount: state.main.totalCount + (state.additional?.totalCount ?? 0), completed: state.main.completed && (state.additional?.completed ?? true)) } +func _internal_getSearchMessageCount(account: Account, location: SearchMessagesLocation, query: String) -> Signal { + guard case let .peer(peerId, fromId, _, _, threadId, _, _) = location else { + return .single(nil) + } + return account.postbox.transaction { transaction -> (Api.InputPeer?, Api.InputPeer?) in + var chatPeer = transaction.getPeer(peerId).flatMap(apiInputPeer) + var fromPeer: Api.InputPeer? + if let fromId { + if let value = transaction.getPeer(fromId).flatMap(apiInputPeer) { + fromPeer = value + } else { + chatPeer = nil + } + } + + return (chatPeer, fromPeer) + } + |> mapToSignal { inputPeer, fromPeer -> Signal in + guard let inputPeer else { + return .single(nil) + } + + var flags: Int32 = 0 + + if let _ = fromPeer { + flags |= (1 << 0) + } + + var topMsgId: Int32? + if let threadId = threadId { + flags |= (1 << 1) + topMsgId = Int32(clamping: threadId) + } + + return account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: query, fromId: fromPeer, savedPeerId: nil, savedReaction: nil, topMsgId: topMsgId, filter: .inputMessagesFilterEmpty, minDate: 0, maxDate: 0, offsetId: 0, addOffset: 0, limit: 1, maxId: 0, minId: 0, hash: 0)) + |> map { result -> Int? in + switch result { + case let .channelMessages(_, _, count, _, _, _, _, _): + return Int(count) + case let .messages(messages, _, _): + return messages.count + case let .messagesNotModified(count): + return Int(count) + case let .messagesSlice(_, count, _, _, _, _, _): + return Int(count) + } + } + |> `catch` { _ -> Signal in + return .single(nil) + } + } +} + func _internal_searchMessages(account: Account, location: SearchMessagesLocation, query: String, state: SearchMessagesState?, centerId: MessageId?, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> { if case let .peer(peerId, fromId, tags, reactions, threadId, minDate, maxDate) = location, fromId == nil, tags == nil, peerId == account.peerId, let reactions, let reaction = reactions.first, (minDate == nil || minDate == 0), (maxDate == nil || maxDate == 0) { return account.postbox.transaction { transaction -> (SearchMessagesResult, SearchMessagesState) in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 38c07f640bc..d26c4114268 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -5,12 +5,12 @@ import TelegramApi public enum EngineStoryInputMedia { case image(dimensions: PixelDimensions, data: Data, stickers: [TelegramMediaFile]) - case video(dimensions: PixelDimensions, duration: Double, resource: TelegramMediaResource, firstFrameFile: TempBoxFile?, stickers: [TelegramMediaFile]) + case video(dimensions: PixelDimensions, duration: Double, resource: TelegramMediaResource, firstFrameFile: TempBoxFile?, stickers: [TelegramMediaFile], coverTime: Double?) case existing(media: Media) var embeddedStickers: [TelegramMediaFile] { switch self { - case let .image(_, _, stickers), let .video(_, _, _, _, stickers): + case let .image(_, _, stickers), let .video(_, _, _, _, stickers, _): return stickers case .existing: return [] @@ -849,7 +849,7 @@ private func prepareUploadStoryContent(account: Account, media: EngineStoryInput flags: [] ) return imageMedia - case let .video(dimensions, duration, resource, firstFrameFile, _): + case let .video(dimensions, duration, resource, firstFrameFile, _, coverTime): var previewRepresentations: [TelegramMediaImageRepresentation] = [] if let firstFrameFile = firstFrameFile { account.postbox.mediaBox.storeCachedResourceRepresentation(resource.id.stringRepresentation, representationId: "first-frame", keepDuration: .general, tempFile: firstFrameFile) @@ -871,7 +871,7 @@ private func prepareUploadStoryContent(account: Account, media: EngineStoryInput mimeType: "video/mp4", size: nil, attributes: [ - TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming, preloadSize: nil) + TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming, preloadSize: nil, coverTime: coverTime) ] ) @@ -1028,6 +1028,7 @@ private struct PendingStoryIdMappingKey: Hashable { } private let pendingStoryIdMapping = Atomic<[PendingStoryIdMappingKey: Int32]>(value: [:]) +private let pendingBotPreviewIdMapping = Atomic<[PendingStoryIdMappingKey: MediaId]>(value: [:]) func _internal_lookUpPendingStoryIdMapping(peerId: PeerId, stableId: Int32) -> Int32? { return pendingStoryIdMapping.with { dict in @@ -1045,6 +1046,22 @@ private func _internal_putPendingStoryIdMapping(peerId: PeerId, stableId: Int32, } } +func _internal_lookUpPendingBotPreviewIdMapping(peerId: PeerId, stableId: Int32) -> MediaId? { + return pendingBotPreviewIdMapping.with { dict in + return dict[PendingStoryIdMappingKey(peerId: peerId, stableId: stableId)] + } +} + +private func _internal_putPendingBotPreviewIdMapping(peerId: PeerId, stableId: Int32, id: MediaId) { + let _ = pendingBotPreviewIdMapping.modify { dict in + var dict = dict + + dict[PendingStoryIdMappingKey(peerId: peerId, stableId: stableId)] = id + + return dict + } +} + func _internal_uploadStoryImpl( postbox: Postbox, network: Network, @@ -1261,6 +1278,227 @@ func _internal_uploadStoryImpl( } } +func _internal_uploadBotPreviewImpl( + postbox: Postbox, + network: Network, + accountPeerId: PeerId, + stateManager: AccountStateManager, + messageMediaPreuploadManager: MessageMediaPreuploadManager, + revalidationContext: MediaReferenceRevalidationContext, + auxiliaryMethods: AccountAuxiliaryMethods, + toPeerId: PeerId, + language: String?, + stableId: Int32, + media: Media, + mediaAreas: [MediaArea], + text: String, + entities: [MessageTextEntity], + embeddedStickers: [TelegramMediaFile], + randomId: Int64 +) -> Signal { + return postbox.transaction { transaction -> Api.InputUser? in + if let peer = transaction.getPeer(toPeerId) { + return apiInputUser(peer) + } + return nil + } + |> mapToSignal { inputUser -> Signal in + guard let inputUser else { + return .single(.completed(nil)) + } + + let passFetchProgress = media is TelegramMediaFile + let (contentSignal, originalMedia) = uploadedStoryContent(postbox: postbox, network: network, media: media, mediaReference: nil, embeddedStickers: embeddedStickers, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods, passFetchProgress: passFetchProgress) + return contentSignal + |> mapToSignal { result -> Signal in + switch result { + case let .progress(progress): + return .single(.progress(progress.progress)) + case let .content(content): + return postbox.transaction { transaction -> Signal in + switch content.content { + case let .media(inputMedia, _): + return network.request(Api.functions.bots.addPreviewMedia(bot: inputUser, langCode: language ?? "", media: inputMedia)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { resultPreviewMedia -> Signal in + guard let resultPreviewMedia else { + return .single(.completed(nil)) + } + switch resultPreviewMedia { + case let .botPreviewMedia(date, resultMedia): + return postbox.transaction { transaction -> StoryUploadResult in + var currentState: Stories.LocalState + if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) { + currentState = value + } else { + currentState = Stories.LocalState(items: []) + } + if let index = currentState.items.firstIndex(where: { $0.stableId == stableId }) { + currentState.items.remove(at: index) + transaction.setLocalStoryState(state: CodableEntry(currentState)) + } + + if let resultMediaValue = textMediaAndExpirationTimerFromApiMedia(resultMedia, toPeerId).media { + applyMediaResourceChanges(from: originalMedia, to: resultMediaValue, postbox: postbox, force: originalMedia is TelegramMediaFile && resultMediaValue is TelegramMediaFile) + + let addedItem = CachedUserData.BotPreview.Item(media: resultMediaValue, timestamp: date) + + if let mediaId = resultMediaValue.id { + _internal_putPendingBotPreviewIdMapping(peerId: toPeerId, stableId: stableId, id: mediaId) + } + + if language == nil { + transaction.updatePeerCachedData(peerIds: Set([toPeerId]), update: { _, current in + guard var current = current as? CachedUserData else { + return current + } + guard let currentBotPreview = current.botPreview else { + return current + } + var items = currentBotPreview.items + if let index = items.firstIndex(where: { $0.media.id == resultMediaValue.id }) { + items.remove(at: index) + } + items.insert(addedItem, at: 0) + let botPreview = CachedUserData.BotPreview(items: items, alternativeLanguageCodes: currentBotPreview.alternativeLanguageCodes) + current = current.withUpdatedBotPreview(botPreview) + return current + }) + } + stateManager.injectBotPreviewUpdates(updates: [ + .added(peerId: toPeerId, language: language, item: addedItem) + ]) + } + + return .completed(nil) + } + } + } + default: + return .complete() + } + } + |> switchToLatest + default: + return .complete() + } + } + } +} + +func _internal_deleteBotPreviews(account: Account, peerId: PeerId, language: String?, media: [Media]) -> Signal { + return account.postbox.transaction { transaction -> (Api.InputUser?, [Api.InputMedia]) in + guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputUser) else { + return (nil, []) + } + + var inputMedia: [Api.InputMedia] = [] + for item in media { + if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource { + inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) + inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) + } else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource { + inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil)) + } + } + if language == nil { + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current -> CachedPeerData? in + guard var current = current as? CachedUserData else { + return current + } + guard let currentBotPreview = current.botPreview else { + return current + } + var items = currentBotPreview.items + + items = items.filter({ item in + guard let id = item.media.id else { + return false + } + return !media.contains(where: { $0.id == id }) + }) + let botPreview = CachedUserData.BotPreview(items: items, alternativeLanguageCodes: currentBotPreview.alternativeLanguageCodes) + current = current.withUpdatedBotPreview(botPreview) + return current + }) + } + + return (inputPeer, inputMedia) + } + |> mapToSignal { inputPeer, inputMedia -> Signal in + guard let inputPeer else { + return .complete() + } + + account.stateManager.injectBotPreviewUpdates(updates: [ + .deleted(peerId: peerId, language: language, ids: media.compactMap(\.id)) + ]) + + return account.network.request(Api.functions.bots.deletePreviewMedia(bot: inputPeer, langCode: language ?? "", media: inputMedia)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } +} + +func _internal_deleteBotPreviewsLanguage(account: Account, peerId: PeerId, language: String, media: [Media]) -> Signal { + return account.postbox.transaction { transaction -> (Api.InputUser?, [Api.InputMedia]) in + guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputUser) else { + return (nil, []) + } + + var inputMedia: [Api.InputMedia] = [] + for item in media { + if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource { + inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) + inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) + } else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource { + inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil)) + } + } + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current -> CachedPeerData? in + guard var current = current as? CachedUserData else { + return current + } + guard let currentBotPreview = current.botPreview else { + return current + } + var alternativeLanguageCodes = currentBotPreview.alternativeLanguageCodes + alternativeLanguageCodes = alternativeLanguageCodes.filter { item in + return item != language + } + let botPreview = CachedUserData.BotPreview(items: currentBotPreview.items, alternativeLanguageCodes: alternativeLanguageCodes) + current = current.withUpdatedBotPreview(botPreview) + return current + }) + + return (inputPeer, inputMedia) + } + |> mapToSignal { inputPeer, inputMedia -> Signal in + guard let inputPeer else { + return .complete() + } + + account.stateManager.injectBotPreviewUpdates(updates: [ + .deleted(peerId: peerId, language: language, ids: media.compactMap(\.id)) + ]) + + return account.network.request(Api.functions.bots.deletePreviewMedia(bot: inputPeer, langCode: language, media: inputMedia)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } +} + func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: EngineStoryInputMedia?, mediaAreas: [MediaArea]?, text: String?, entities: [MessageTextEntity]?, privacy: EngineStoryPrivacy?) -> Signal { let contentSignal: Signal let originalMedia: Media? @@ -1281,9 +1519,13 @@ func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: Eng return .single(.progress(progress.progress)) } + var updatingCoverTime = false let inputMedia: Api.InputMedia? if let result = result, case let .content(uploadedContent) = result, case let .media(media, _) = uploadedContent.content { inputMedia = media + } else if case let .existing(media) = media, let file = media as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource { + inputMedia = .inputMediaUploadedDocument(flags: 0, file: .inputFileStoryDocument(id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference))), thumb: nil, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: nil, ttlSeconds: nil) + updatingCoverTime = true } else { inputMedia = nil } @@ -1349,7 +1591,7 @@ func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: Eng case let .storyItem(_, _, _, _, _, _, _, _, media, _, _, _, _): let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId) if let parsedMedia = parsedMedia, let originalMedia = originalMedia { - applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false) + applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false, skipPreviews: updatingCoverTime) } default: break @@ -1472,6 +1714,8 @@ func _internal_checkStoriesUploadAvailability(account: Account, target: Stories. return .inputPeerSelf case let .peer(peerId): return transaction.getPeer(peerId).flatMap(apiInputPeer) + case .botPreview: + return nil } } |> mapToSignal { inputPeer -> Signal in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 4357f346e41..e22036f50ad 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -12,6 +12,11 @@ enum InternalStoryUpdate { case updateMyReaction(peerId: PeerId, id: Int32, reaction: MessageReaction.Reaction?) } +enum InternalBotPreviewUpdate { + case added(peerId: PeerId, language: String?, item: CachedUserData.BotPreview.Item) + case deleted(peerId: PeerId, language: String?, ids: [MediaId]) +} + public final class EngineStoryItem: Equatable { public final class Views: Equatable { public let seenCount: Int @@ -563,9 +568,20 @@ public struct StoryListContextState: Equatable { } } + public struct Language: Equatable { + public let id: String + public let name: String + + public init(id: String, name: String) { + self.id = id + self.name = name + } + } + public var peerReference: PeerReference? public var items: [Item] - public var pinnedIds: Set + public var availableLanguages: [Language] + public var pinnedIds: [Int32] public var totalCount: Int public var loadMoreToken: AnyHashable? public var isCached: Bool @@ -575,7 +591,8 @@ public struct StoryListContextState: Equatable { public init( peerReference: PeerReference?, items: [Item], - pinnedIds: Set, + availableLanguages: [Language], + pinnedIds: [Int32], totalCount: Int, loadMoreToken: AnyHashable?, isCached: Bool, @@ -585,6 +602,7 @@ public struct StoryListContextState: Equatable { ) { self.peerReference = peerReference self.items = items + self.availableLanguages = availableLanguages self.pinnedIds = pinnedIds self.totalCount = totalCount self.loadMoreToken = loadMoreToken @@ -633,7 +651,7 @@ public final class PeerStoryListContext: StoryListContext { self.peerId = peerId self.isArchived = isArchived - self.stateValue = State(peerReference: nil, items: [], pinnedIds: Set(), totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false) + self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false) let _ = (account.postbox.transaction { transaction -> (PeerReference?, [State.Item], [Int32], Int, [MediaId: TelegramMediaFile], Bool) in let key = ValueBoxKey(length: 8 + 1) @@ -723,17 +741,19 @@ public final class PeerStoryListContext: StoryListContext { return } - var updatedState = State(peerReference: peerReference, items: items, pinnedIds: Set(pinnedIds), totalCount: totalCount, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: hasCache, allEntityFiles: allEntityFiles, isLoading: false) + var updatedState = State(peerReference: peerReference, items: items, availableLanguages: [], pinnedIds: pinnedIds, totalCount: totalCount, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: hasCache, allEntityFiles: allEntityFiles, isLoading: false) updatedState.items.sort(by: { lhs, rhs in - let lhsPinned = updatedState.pinnedIds.contains(lhs.storyItem.id) - let rhsPinned = updatedState.pinnedIds.contains(rhs.storyItem.id) - if lhsPinned != rhsPinned { - if lhsPinned { - return true - } else { - return false + let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id) + let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id) + + if let lhsPinned, let rhsPinned { + if lhsPinned != rhsPinned { + return lhsPinned < rhsPinned } + } else if (lhsPinned == nil) != (rhsPinned == nil) { + return lhsPinned != nil } + return lhs.storyItem.timestamp > rhs.storyItem.timestamp }) self.stateValue = updatedState @@ -744,6 +764,7 @@ public final class PeerStoryListContext: StoryListContext { deinit { self.requestDisposable?.dispose() + self.updatesDisposable?.dispose() } func loadMore(completion: (() -> Void)?) { @@ -862,7 +883,7 @@ public final class PeerStoryListContext: StoryListContext { let key = ValueBoxKey(length: 8 + 1) key.setInt64(0, value: peerId.toInt64()) key.setInt8(8, value: isArchived ? 1 : 0) - if let entry = CodableEntry(CachedPeerStoryListHead(items: storyItems.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: Array(pinnedIds), totalCount: count)) { + if let entry = CodableEntry(CachedPeerStoryListHead(items: storyItems.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: count)) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) } } @@ -1124,14 +1145,15 @@ public final class PeerStoryListContext: StoryListContext { author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } ), peer: nil)) updatedState.items.sort(by: { lhs, rhs in - let lhsPinned = updatedState.pinnedIds.contains(lhs.storyItem.id) - let rhsPinned = updatedState.pinnedIds.contains(rhs.storyItem.id) - if lhsPinned != rhsPinned { - if lhsPinned { - return true - } else { - return false + let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id) + let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id) + + if let lhsPinned, let rhsPinned { + if lhsPinned != rhsPinned { + return lhsPinned < rhsPinned } + } else if (lhsPinned == nil) != (rhsPinned == nil) { + return lhsPinned != nil } return lhs.storyItem.timestamp > rhs.storyItem.timestamp }) @@ -1180,14 +1202,15 @@ public final class PeerStoryListContext: StoryListContext { author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } ), peer: nil)) updatedState.items.sort(by: { lhs, rhs in - let lhsPinned = updatedState.pinnedIds.contains(lhs.storyItem.id) - let rhsPinned = updatedState.pinnedIds.contains(rhs.storyItem.id) - if lhsPinned != rhsPinned { - if lhsPinned { - return true - } else { - return false + let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id) + let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id) + + if let lhsPinned, let rhsPinned { + if lhsPinned != rhsPinned { + return lhsPinned < rhsPinned } + } else if (lhsPinned == nil) != (rhsPinned == nil) { + return lhsPinned != nil } return lhs.storyItem.timestamp > rhs.storyItem.timestamp }) @@ -1204,18 +1227,19 @@ public final class PeerStoryListContext: StoryListContext { case let .updatePinnedToTopList(peerId, ids): if self.peerId == peerId && !self.isArchived { let previousIds = (finalUpdatedState ?? self.stateValue).pinnedIds - if previousIds != Set(ids) { + if previousIds != ids { var updatedState = finalUpdatedState ?? self.stateValue - updatedState.pinnedIds = Set(ids) + updatedState.pinnedIds = ids updatedState.items.sort(by: { lhs, rhs in - let lhsPinned = updatedState.pinnedIds.contains(lhs.storyItem.id) - let rhsPinned = updatedState.pinnedIds.contains(rhs.storyItem.id) - if lhsPinned != rhsPinned { - if lhsPinned { - return true - } else { - return false + let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id) + let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id) + + if let lhsPinned, let rhsPinned { + if lhsPinned != rhsPinned { + return lhsPinned < rhsPinned } + } else if (lhsPinned == nil) != (rhsPinned == nil) { + return lhsPinned != nil } return lhs.storyItem.timestamp > rhs.storyItem.timestamp }) @@ -1235,7 +1259,7 @@ public final class PeerStoryListContext: StoryListContext { let key = ValueBoxKey(length: 8 + 1) key.setInt64(0, value: peerId.toInt64()) key.setInt8(8, value: isArchived ? 1 : 0) - if let entry = CodableEntry(CachedPeerStoryListHead(items: items.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: Array(pinnedIds), totalCount: Int32(totalCount))) { + if let entry = CodableEntry(CachedPeerStoryListHead(items: items.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: Int32(totalCount))) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry) } }).start() @@ -1308,7 +1332,7 @@ public final class SearchStoryListContext: StoryListContext { self.account = account self.source = source - self.stateValue = State(peerReference: nil, items: [], pinnedIds: Set(), totalCount: 0, loadMoreToken: AnyHashable(""), isCached: false, hasCache: false, allEntityFiles: [:], isLoading: false) + self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(""), isCached: false, hasCache: false, allEntityFiles: [:], isLoading: false) self.statePromise.set(.single(self.stateValue)) self.loadMore(completion: nil) @@ -2066,3 +2090,632 @@ public func _internal_pollPeerStories(postbox: Postbox, network: Network, accoun } } } + +public final class BotPreviewStoryListContext: StoryListContext { + private final class Impl { + private let queue: Queue + private let account: Account + private let engine: TelegramEngine + private let peerId: EnginePeer.Id + private let language: String? + private let isArchived: Bool + + private let statePromise = Promise() + private var stateValue: State { + didSet { + self.statePromise.set(.single(self.stateValue)) + } + } + var state: Signal { + return self.statePromise.get() + } + + private var isLoadingMore: Bool = false + private var requestDisposable: Disposable? + private var updatesDisposable: Disposable? + private var eventsDisposable: Disposable? + private let reorderDisposable = MetaDisposable() + + private var completionCallbacksByToken: [AnyHashable: [() -> Void]] = [:] + + private var nextId: Int32 = 1 + private var pendingIdMapping: [Int32: Int32] = [:] + private var idMapping: [MediaId: Int32] = [:] + private var reverseIdMapping: [Int32: MediaId] = [:] + + private var localItems: [State.Item] = [] + private var remoteItems: [State.Item] = [] + + init(queue: Queue, account: Account, engine: TelegramEngine, peerId: EnginePeer.Id, language: String?, assumeEmpty: Bool) { + self.queue = queue + self.account = account + self.engine = engine + self.peerId = peerId + self.language = language + + let isArchived = false + + self.isArchived = isArchived + + self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false) + + let localStateKey: PostboxViewKey = .storiesState(key: .local) + + if let language { + let _ = (account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> deliverOn(self.queue)).start(next: { [weak self] peer in + guard let self else { + return + } + + self.stateValue = State( + peerReference: peer.flatMap(PeerReference.init), + items: [], + availableLanguages: [], + pinnedIds: [], + totalCount: 0, + loadMoreToken: AnyHashable(0), + isCached: assumeEmpty, + hasCache: assumeEmpty, + allEntityFiles: [:], + isLoading: !assumeEmpty + ) + + self.loadLanguage(language: language, assumeEmpty: assumeEmpty) + }) + } else { + self.requestDisposable = (combineLatest(queue: queue, + engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), + TelegramEngine.EngineData.Item.Peer.BotPreview(id: peerId), + TelegramEngine.EngineData.Item.Configuration.LocalizationList() + ), + account.postbox.combinedView(keys: [ + localStateKey + ]) + ) + |> deliverOn(self.queue)).start(next: { [weak self] peerAndBotPreview, combinedView in + guard let self else { + return + } + + let (peer, botPreview, localizationList) = peerAndBotPreview + + var items: [State.Item] = [] + var availableLanguages: [StoryListContextState.Language] = [] + + if let stateView = combinedView.views[localStateKey] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) { + for item in localState.items.reversed() { + let mappedId: Int32 + if let current = self.pendingIdMapping[item.stableId] { + mappedId = current + } else { + mappedId = self.nextId + self.nextId += 1 + self.pendingIdMapping[item.stableId] = mappedId + } + + if let mediaId = _internal_lookUpPendingBotPreviewIdMapping(peerId: self.peerId, stableId: item.stableId) { + if let botPreview, botPreview.items.contains(where: { $0.media.id == mediaId }) { + continue + } + } + + if case let .botPreview(itemPeerId, itemLanguage) = item.target, itemPeerId == peerId, itemLanguage == language { + items.append(State.Item( + id: StoryId(peerId: peerId, id: mappedId), + storyItem: EngineStoryItem( + id: mappedId, + timestamp: 0, + expirationTimestamp: Int32.max, + media: EngineMedia(item.media), + alternativeMedia: nil, + mediaAreas: [], + text: "", + entities: [], + views: nil, + privacy: nil, + isPinned: false, + isExpired: false, + isPublic: false, + isPending: true, + isCloseFriends: false, + isContacts: false, + isSelectedContacts: false, + isForwardingDisabled: false, + isEdited: false, + isMy: false, + myReaction: nil, + forwardInfo: nil, + author: nil + ), + peer: nil + )) + } + } + } + + if let botPreview { + for item in botPreview.items { + guard let mediaId = item.media.id else { + continue + } + + let id: Int32 + if let current = self.idMapping[mediaId] { + id = current + } else { + id = self.nextId + self.nextId += 1 + self.idMapping[mediaId] = id + self.reverseIdMapping[id] = mediaId + } + + items.append(State.Item( + id: StoryId(peerId: peerId, id: id), + storyItem: EngineStoryItem( + id: id, + timestamp: item.timestamp, + expirationTimestamp: Int32.max, + media: EngineMedia(item.media), + alternativeMedia: nil, + mediaAreas: [], + text: "", + entities: [], + views: nil, + privacy: nil, + isPinned: false, + isExpired: false, + isPublic: false, + isPending: false, + isCloseFriends: false, + isContacts: false, + isSelectedContacts: false, + isForwardingDisabled: false, + isEdited: false, + isMy: false, + myReaction: nil, + forwardInfo: nil, + author: nil + ), + peer: nil + )) + } + + for id in botPreview.alternativeLanguageCodes { + inner: for localization in localizationList.availableOfficialLocalizations { + if localization.languageCode == id { + availableLanguages.append(StoryListContextState.Language( + id: localization.languageCode, + name: localization.title + )) + break inner + } + } + } + } + + self.stateValue = State( + peerReference: (peer?._asPeer()).flatMap(PeerReference.init), + items: items, + availableLanguages: availableLanguages, + pinnedIds: [], + totalCount: items.count, + loadMoreToken: nil, + isCached: botPreview != nil, + hasCache: botPreview != nil, + allEntityFiles: [:], + isLoading: botPreview == nil + ) + }) + } + } + + deinit { + self.requestDisposable?.dispose() + self.updatesDisposable?.dispose() + self.eventsDisposable?.dispose() + self.reorderDisposable.dispose() + } + + func loadMore(completion: (() -> Void)?) { + } + + private func loadLanguage(language: String, assumeEmpty: Bool) { + let account = self.account + let peerId = self.peerId + let signal: Signal<(CachedUserData.BotPreview?, Peer?), NoError> = (self.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> mapToSignal { peer -> Signal<(CachedUserData.BotPreview?, Peer?), NoError> in + guard let peer, let inputUser = apiInputUser(peer) else { + return .single((nil, nil)) + } + return _internal_requestBotAdminPreview(network: account.network, peerId: peerId, inputUser: inputUser, language: language) + |> map { botPreview in + return (botPreview, peer) + } + }) + + self.requestDisposable?.dispose() + self.requestDisposable = (signal + |> deliverOn(self.queue)).startStrict(next: { [weak self] botPreview, peer in + guard let self, let peer else { + return + } + + var items: [State.Item] = [] + + if let botPreview { + for item in botPreview.items { + guard let mediaId = item.media.id else { + continue + } + + let id: Int32 + if let current = self.idMapping[mediaId] { + id = current + } else { + id = self.nextId + self.nextId += 1 + self.idMapping[mediaId] = id + self.reverseIdMapping[id] = mediaId + } + + items.append(State.Item( + id: StoryId(peerId: peerId, id: id), + storyItem: EngineStoryItem( + id: id, + timestamp: item.timestamp, + expirationTimestamp: Int32.max, + media: EngineMedia(item.media), + alternativeMedia: nil, + mediaAreas: [], + text: "", + entities: [], + views: nil, + privacy: nil, + isPinned: false, + isExpired: false, + isPublic: false, + isPending: false, + isCloseFriends: false, + isContacts: false, + isSelectedContacts: false, + isForwardingDisabled: false, + isEdited: false, + isMy: false, + myReaction: nil, + forwardInfo: nil, + author: nil + ), + peer: nil + )) + } + } + + self.remoteItems = items + self.stateValue = State( + peerReference: PeerReference(peer), + items: items, + availableLanguages: [], + pinnedIds: [], + totalCount: items.count, + loadMoreToken: nil, + isCached: botPreview != nil, + hasCache: botPreview != nil, + allEntityFiles: [:], + isLoading: botPreview == nil + ) + + if botPreview != nil { + self.beginUpdates(language: language) + } + }) + } + + private func beginUpdates(language: String) { + let localStateKey: PostboxViewKey = .storiesState(key: .local) + + self.updatesDisposable?.dispose() + self.updatesDisposable = (self.account.postbox.combinedView(keys: [ + localStateKey + ]) + |> deliverOn(self.queue)).startStrict(next: { [weak self] combinedView in + guard let self else { + return + } + + var items: [State.Item] = [] + if let stateView = combinedView.views[localStateKey] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) { + for item in localState.items.reversed() { + let mappedId: Int32 + if let current = self.pendingIdMapping[item.stableId] { + mappedId = current + } else { + mappedId = self.nextId + self.nextId += 1 + self.pendingIdMapping[item.stableId] = mappedId + } + if case let .botPreview(itemPeerId, itemLanguage) = item.target, itemPeerId == self.peerId, itemLanguage == language { + items.append(State.Item( + id: StoryId(peerId: peerId, id: mappedId), + storyItem: EngineStoryItem( + id: mappedId, + timestamp: 0, + expirationTimestamp: Int32.max, + media: EngineMedia(item.media), + alternativeMedia: nil, + mediaAreas: [], + text: "", + entities: [], + views: nil, + privacy: nil, + isPinned: false, + isExpired: false, + isPublic: false, + isPending: true, + isCloseFriends: false, + isContacts: false, + isSelectedContacts: false, + isForwardingDisabled: false, + isEdited: false, + isMy: false, + myReaction: nil, + forwardInfo: nil, + author: nil + ), + peer: nil + )) + } + } + } + + if self.localItems != items { + self.localItems = items + + if self.stateValue.peerReference != nil { + self.pushLanguageItems() + } + } + }) + + self.eventsDisposable?.dispose() + self.eventsDisposable = (self.account.stateManager.botPreviewUpdates + |> deliverOn(self.queue)).startStrict(next: { [weak self] events in + guard let self else { + return + } + var remoteItems = self.remoteItems + for event in events { + switch event { + case let .added(peerId, language, item): + if let mediaId = item.media.id, self.peerId == peerId, self.language == language { + let id: Int32 + if let current = self.idMapping[mediaId] { + id = current + } else { + id = self.nextId + self.nextId += 1 + self.idMapping[mediaId] = id + self.reverseIdMapping[id] = mediaId + } + + let mappedItem = State.Item( + id: StoryId(peerId: peerId, id: id), + storyItem: EngineStoryItem( + id: id, + timestamp: item.timestamp, + expirationTimestamp: Int32.max, + media: EngineMedia(item.media), + alternativeMedia: nil, + mediaAreas: [], + text: "", + entities: [], + views: nil, + privacy: nil, + isPinned: false, + isExpired: false, + isPublic: false, + isPending: false, + isCloseFriends: false, + isContacts: false, + isSelectedContacts: false, + isForwardingDisabled: false, + isEdited: false, + isMy: false, + myReaction: nil, + forwardInfo: nil, + author: nil + ), + peer: nil + ) + + if let index = remoteItems.firstIndex(where: { $0.storyItem.media.id == item.media.id }) { + remoteItems[index] = mappedItem + } else { + remoteItems.insert(mappedItem, at: 0) + } + } + case let .deleted(peerId, language, ids): + if self.peerId == peerId && self.language == language { + remoteItems = remoteItems.filter { item in + guard let id = item.storyItem.media.id else { + return false + } + return !ids.contains(id) + } + } + } + } + if self.remoteItems != remoteItems { + self.remoteItems = remoteItems + self.pushLanguageItems() + } + }) + } + + private func pushLanguageItems() { + var items: [State.Item] = [] + for item in self.localItems { + var stableId: Int32? + inner: for (from, to) in self.pendingIdMapping { + if to == item.id.id { + stableId = from + break inner + } + } + if let stableId, let mediaId = _internal_lookUpPendingBotPreviewIdMapping(peerId: self.peerId, stableId: stableId) { + if self.remoteItems.contains(where: { $0.storyItem.media.id == mediaId }) { + continue + } + } + items.append(item) + } + items.append(contentsOf: self.remoteItems) + + self.stateValue = State( + peerReference: self.stateValue.peerReference, + items: items, + availableLanguages: [], + pinnedIds: [], + totalCount: items.count, + loadMoreToken: nil, + isCached: true, + hasCache: true, + allEntityFiles: [:], + isLoading: false + ) + } + + func reorderItems(media: [Media]) { + let peerId = self.peerId + let language = self.language + + let _ = (self.account.postbox.transaction({ transaction -> (Api.InputUser?, [Api.InputMedia]) in + let inputUser = transaction.getPeer(peerId).flatMap(apiInputUser) + + var inputMedia: [Api.InputMedia] = [] + for item in media { + if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource { + inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) + inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) + } else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource { + inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil)) + } + } + + if language == nil { + transaction.updatePeerCachedData(peerIds: Set([self.peerId]), update: { _, current in + guard var current = current as? CachedUserData else { + return current + } + guard let currentBotPreview = current.botPreview else { + return current + } + + var items: [CachedUserData.BotPreview.Item] = [] + + var seenIds = Set() + for item in media { + guard let mediaId = item.id else { + continue + } + if let index = currentBotPreview.items.firstIndex(where: { $0.media.id == mediaId }) { + seenIds.insert(mediaId) + items.append(currentBotPreview.items[index]) + } + } + + for item in currentBotPreview.items { + guard let id = item.media.id else { + continue + } + if !seenIds.contains(id) { + items.append(item) + } + } + + let botPreview = CachedUserData.BotPreview(items: items, alternativeLanguageCodes: currentBotPreview.alternativeLanguageCodes) + current = current.withUpdatedBotPreview(botPreview) + return current + }) + } + + return (inputUser, inputMedia) + }) + |> deliverOn(self.queue)).startStandalone(next: { [weak self] inputUser, inputMedia in + guard let self, let inputUser else { + return + } + + if language != nil { + var updatedItems: [State.Item] = [] + + var seenIds = Set() + for item in media { + guard let mediaId = item.id else { + continue + } + if let index = self.remoteItems.firstIndex(where: { $0.storyItem.media.id == mediaId }) { + seenIds.insert(mediaId) + updatedItems.append(self.remoteItems[index]) + } + } + + for item in self.remoteItems { + guard let id = item.storyItem.media.id else { + continue + } + if !seenIds.contains(id) { + updatedItems.append(item) + } + } + + if self.remoteItems != updatedItems { + self.remoteItems = updatedItems + self.pushLanguageItems() + } + } + + let signal = self.account.network.request(Api.functions.bots.reorderPreviewMedias(bot: inputUser, langCode: language ?? "", order: inputMedia)) + self.reorderDisposable.set(signal.startStrict()) + }) + } + } + + public var state: Signal { + return impl.signalWith { impl, subscriber in + return impl.state.start(next: subscriber.putNext) + } + } + + private let queue: Queue + private let impl: QueueLocalObject + + public let language: String? + + public init(account: Account, engine: TelegramEngine, peerId: EnginePeer.Id, language: String?, assumeEmpty: Bool) { + self.language = language + + let queue = Queue.mainQueue() + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, account: account, engine: engine, peerId: peerId, language: language, assumeEmpty: assumeEmpty) + }) + } + + public func loadMore(completion: (() -> Void)? = nil) { + self.impl.with { impl in + impl.loadMore(completion: completion) + } + } + + public func reorderItems(media: [Media]) { + self.impl.with { impl in + impl.reorderItems(media: media) + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index d2ab82fa6d5..31c562b56d2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -76,6 +76,10 @@ public extension TelegramEngine { return _internal_searchMessages(account: self.account, location: location, query: query, state: state, centerId: centerId, limit: limit) } + public func getSearchMessageCount(location: SearchMessagesLocation, query: String) -> Signal { + return _internal_getSearchMessageCount(account: self.account, location: location, query: query) + } + public func searchHashtagPosts(hashtag: String, state: SearchMessagesState?, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> { return _internal_searchHashtagPosts(account: self.account, hashtag: hashtag, state: state, limit: limit) } @@ -584,6 +588,10 @@ public extension TelegramEngine { return _internal_requestSimpleWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, url: url, source: source, themeParams: themeParams) } + public func requestMainWebView(botId: PeerId, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { + return _internal_requestMainWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, source: source, themeParams: themeParams) + } + public func requestAppWebView(peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, compact: Bool, allowWrite: Bool) -> Signal { return _internal_requestAppWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, appReference: appReference, payload: payload, themeParams: themeParams, compact: compact, allowWrite: allowWrite) } @@ -1379,6 +1387,14 @@ public extension TelegramEngine { return _internal_getStoryById(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, peerId: peerId, id: id) } + public func deleteBotPreviews(peerId: EnginePeer.Id, language: String?, media: [Media]) -> Signal { + return _internal_deleteBotPreviews(account: self.account, peerId: peerId, language: language, media: media) + } + + public func deleteBotPreviewsLanguage(peerId: EnginePeer.Id, language: String, media: [Media]) -> Signal { + return _internal_deleteBotPreviewsLanguage(account: self.account, peerId: peerId, language: language, media: media) + } + public func synchronouslyIsMessageDeletedInteractively(ids: [EngineMessage.Id]) -> [EngineMessage.Id] { return self.account.stateManager.synchronouslyIsMessageDeletedInteractively(ids: ids) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift index 32a13749310..f802d15ce9d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift @@ -18,6 +18,7 @@ public enum AppStoreTransactionPurpose { case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64) case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64) case stars(count: Int64, currency: String, amount: Int64) + case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64) } private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTransactionPurpose) -> Signal { @@ -91,7 +92,15 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran } |> switchToLatest case let .stars(count, currency, amount): - return .single(.inputStorePaymentStars(flags: 0, stars: count, currency: currency, amount: amount)) + return .single(.inputStorePaymentStarsTopup(stars: count, currency: currency, amount: amount)) + case let .starsGift(peerId, count, currency, amount): + return account.postbox.loadedPeerWithId(peerId) + |> mapToSignal { peer -> Signal in + guard let inputUser = apiInputUser(peer) else { + return .complete() + } + return .single(.inputStorePaymentStarsGift(userId: inputUser, stars: count, currency: currency, amount: amount)) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 883b9b7351b..d5175d928dc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -10,6 +10,7 @@ public enum BotPaymentInvoiceSource { case premiumGiveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, option: PremiumGiftCodeOption) 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) } public struct BotPaymentInvoiceFields: OptionSet { @@ -307,9 +308,12 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv if let _ = option.storeProductId { flags |= (1 << 0) } - return .inputInvoiceStars( - option: .starsTopupOption(flags: flags, stars: option.count, storeProduct: option.storeProductId, currency: option.currency, amount: option.amount) - ) + return .inputInvoiceStars(purpose: .inputStorePaymentStarsTopup(stars: option.count, currency: option.currency, amount: option.amount)) + case let .starsGift(peerId, count, currency, amount): + guard let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) else { + return nil + } + return .inputInvoiceStars(purpose: .inputStorePaymentStarsGift(userId: inputUser, stars: count, currency: currency, amount: amount)) } } @@ -608,9 +612,7 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa receiptMessageId = id } } - case .giftCode: - receiptMessageId = nil - case .stars: + case .giftCode, .stars, .starsGift: receiptMessageId = nil } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 43b9709bc98..0bf8b5549c3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -4,24 +4,27 @@ import MtProtoKit import SwiftSignalKit import TelegramApi -public struct StarsTopUpOption: Codable, Equatable { +public struct StarsTopUpOption: Equatable, Codable { enum CodingKeys: String, CodingKey { case count case storeProductId case currency case amount + case isExtended } public let count: Int64 public let storeProductId: String? public let currency: String public let amount: Int64 + public let isExtended: Bool - public init(count: Int64, storeProductId: String?, currency: String, amount: Int64) { + public init(count: Int64, storeProductId: String?, currency: String, amount: Int64, isExtended: Bool) { self.count = count self.storeProductId = storeProductId self.currency = currency self.amount = amount + self.isExtended = isExtended } public init(from decoder: Decoder) throws { @@ -30,7 +33,7 @@ public struct StarsTopUpOption: Codable, Equatable { self.storeProductId = try container.decodeIfPresent(String.self, forKey: .storeProductId) self.currency = try container.decode(String.self, forKey: .currency) self.amount = try container.decode(Int64.self, forKey: .amount) - + self.isExtended = try container.decodeIfPresent(Bool.self, forKey: .isExtended) ?? false } public func encode(to encoder: Encoder) throws { @@ -39,14 +42,15 @@ public struct StarsTopUpOption: Codable, Equatable { try container.encodeIfPresent(self.storeProductId, forKey: .storeProductId) try container.encode(self.currency, forKey: .currency) try container.encode(self.amount, forKey: .amount) + try container.encode(self.isExtended, forKey: .isExtended) } } extension StarsTopUpOption { init(apiStarsTopupOption: Api.StarsTopupOption) { switch apiStarsTopupOption { - case let .starsTopupOption(_, stars, storeProduct, currency, amount): - self.init(count: stars, storeProductId: storeProduct, currency: currency, amount: amount) + case let .starsTopupOption(flags, stars, storeProduct, currency, amount): + self.init(count: stars, storeProductId: storeProduct, currency: currency, amount: amount, isExtended: (flags & (1 << 1)) != 0) } } } @@ -66,6 +70,81 @@ func _internal_starsTopUpOptions(account: Account) -> Signal<[StarsTopUpOption], } } +public struct StarsGiftOption: Equatable, Codable { + enum CodingKeys: String, CodingKey { + case count + case currency + case amount + case storeProductId + case isExtended + } + + public let count: Int64 + public let currency: String + public let amount: Int64 + public let storeProductId: String? + public let isExtended: Bool + + public init(count: Int64, storeProductId: String?, currency: String, amount: Int64, isExtended: Bool) { + self.count = count + self.currency = currency + self.amount = amount + self.storeProductId = storeProductId + self.isExtended = isExtended + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.count = try container.decode(Int64.self, forKey: .count) + self.storeProductId = try container.decodeIfPresent(String.self, forKey: .storeProductId) + self.currency = try container.decode(String.self, forKey: .currency) + self.amount = try container.decode(Int64.self, forKey: .amount) + self.isExtended = try container.decodeIfPresent(Bool.self, forKey: .isExtended) ?? false + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.count, forKey: .count) + try container.encodeIfPresent(self.storeProductId, forKey: .storeProductId) + try container.encode(self.currency, forKey: .currency) + try container.encode(self.amount, forKey: .amount) + try container.encode(self.isExtended, forKey: .isExtended) + } +} + +extension StarsGiftOption { + init(apiStarsGiftOption: Api.StarsGiftOption) { + switch apiStarsGiftOption { + case let .starsGiftOption(flags, stars, storeProduct, currency, amount): + self.init(count: stars, storeProductId: storeProduct, currency: currency, amount: amount, isExtended: (flags & (1 << 1)) != 0) + } + } +} + +func _internal_starsGiftOptions(account: Account, peerId: EnginePeer.Id?) -> Signal<[StarsGiftOption], NoError> { + return account.postbox.transaction { transaction -> Api.InputUser? in + return peerId.flatMap { transaction.getPeer($0).flatMap(apiInputUser) } + } + |> mapToSignal { inputUser in + var flags: Int32 = 0 + if let _ = inputUser { + flags |= (1 << 0) + } + return account.network.request(Api.functions.payments.getStarsGiftOptions(flags: flags, userId: inputUser)) + |> map(Optional.init) + |> `catch` { _ -> Signal<[Api.StarsGiftOption]?, NoError> in + return .single(nil) + } + |> mapToSignal { results -> Signal<[StarsGiftOption], NoError> in + if let results = results { + return .single(results.map { StarsGiftOption(apiStarsGiftOption: $0) }) + } else { + return .single([]) + } + } + } +} + struct InternalStarsStatus { let balance: Int64 let transactions: [StarsContext.State.Transaction] @@ -278,6 +357,9 @@ private extension StarsContext.State.Transaction { if (apiFlags & (1 << 6)) != 0 { flags.insert(.isFailed) } + if (apiFlags & (1 << 10)) != 0 { + flags.insert(.isGift) + } 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) @@ -299,6 +381,7 @@ public final class StarsContext { public static let isLocal = Flags(rawValue: 1 << 1) 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 enum Peer: Equatable { @@ -734,9 +817,7 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot receiptMessageId = id } } - case .giftCode: - receiptMessageId = nil - case .stars: + case .giftCode, .stars, .starsGift: receiptMessageId = nil } } @@ -763,3 +844,59 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot } } } + +public struct StarsTransactionReference: PostboxCoding, Hashable, Equatable { + public let peerId: EnginePeer.Id + public let id: String + public let isRefund: Bool + + public init(peerId: EnginePeer.Id, id: String, isRefund: Bool) { + self.peerId = peerId + self.id = id + self.isRefund = isRefund + } + + public init(decoder: PostboxDecoder) { + self.peerId = EnginePeer.Id(decoder.decodeInt64ForKey("peerId", orElse: 0)) + self.id = decoder.decodeStringForKey("id", orElse: "") + self.isRefund = decoder.decodeBoolForKey("refund", orElse: false) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64(self.peerId.toInt64(), forKey: "peerId") + encoder.encodeString(self.id, forKey: "id") + encoder.encodeBool(self.isRefund, forKey: "refund") + } +} + +func _internal_getStarsTransaction(accountPeerId: PeerId, postbox: Postbox, network: Network, transactionReference: StarsTransactionReference) -> Signal { + return postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(transactionReference.peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .single(nil) + } + return network.request( + Api.functions.payments.getStarsTransactionsByID( + peer: inputPeer, + id: [.inputStarsTransaction(flags: transactionReference.isRefund ? (1 << 0) : 0, id: transactionReference.id)] + ) + ) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> 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 { + return nil + } + let peers = AccumulatedPeers(chats: chats, users: users) + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: peers) + + return StarsContext.State.Transaction(apiTransaction: matchingTransaction, peerId: transactionReference.peerId, transaction: transaction) + } + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 1640d568ca8..062e457ea85 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -70,6 +70,10 @@ public extension TelegramEngine { return _internal_starsTopUpOptions(account: self.account) } + public func starsGiftOptions(peerId: EnginePeer.Id?) -> Signal<[StarsGiftOption], NoError> { + return _internal_starsGiftOptions(account: self.account, peerId: peerId) + } + public func peerStarsContext() -> StarsContext { return StarsContext(account: self.account) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelRecommendation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelRecommendation.swift index 6d186fb8211..ca5df1efee3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelRecommendation.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelRecommendation.swift @@ -46,6 +46,12 @@ private func entryId(peerId: EnginePeer.Id?) -> ItemCacheEntryId { return ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.recommendedChannels, key: cacheKey) } +private func appsEntryId() -> ItemCacheEntryId { + let cacheKey = ValueBoxKey(length: 8) + cacheKey.setInt64(0, value: 0) + return ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.recommendedApps, key: cacheKey) +} + func _internal_requestRecommendedChannels(account: Account, peerId: EnginePeer.Id?, forceUpdate: Bool) -> Signal { return account.postbox.transaction { transaction -> (Peer?, Bool) in if let peerId { @@ -134,6 +140,55 @@ func _internal_requestRecommendedChannels(account: Account, peerId: EnginePeer.I } } +func _internal_requestRecommendedApps(account: Account, forceUpdate: Bool) -> Signal { + return account.postbox.transaction { transaction -> (Peer?, Bool) in + if let entry = transaction.retrieveItemCacheEntry(id: appsEntryId())?.get(CachedRecommendedChannels.self), !entry.peerIds.isEmpty && !forceUpdate { + var shouldUpdate = false + if let timestamp = entry.timestamp { + if timestamp + 60 * 60 < Int32(Date().timeIntervalSince1970) { + shouldUpdate = true + } + } else { + shouldUpdate = true + } + return (nil, shouldUpdate) + } else { + return (nil, true) + } + } + |> mapToSignal { channel, shouldUpdate in + if !shouldUpdate { + return .complete() + } + return account.network.request(Api.functions.bots.getPopularAppBots(offset: "", limit: 100)) + |> retryRequest + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> [EnginePeer] in + let users: [Api.User] + let parsedPeers: AccumulatedPeers + switch result { + case let .popularAppBots(_, nextOffset, apiUsers): + let _ = nextOffset + users = apiUsers + } + parsedPeers = AccumulatedPeers(transaction: transaction, chats: [], users: users) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) + var peers: [EnginePeer] = [] + for user in users { + if let peer = transaction.getPeer(user.peerId) { + peers.append(EnginePeer(peer)) + } + } + if let entry = CodableEntry(CachedRecommendedChannels(peerIds: peers.map(\.id), count: Int32(peers.count), isHidden: false, timestamp: Int32(Date().timeIntervalSince1970))) { + transaction.putItemCacheEntry(id: appsEntryId(), entry: entry) + } + return peers + } + |> ignoreValues + } + } +} + public struct RecommendedChannels: Equatable { public struct Channel: Equatable { public var peer: EnginePeer @@ -167,6 +222,17 @@ func _internal_recommendedChannelPeerIds(account: Account, peerId: EnginePeer.Id } } +func _internal_recommendedAppPeerIds(account: Account) -> Signal<[EnginePeer.Id]?, NoError> { + let key = PostboxViewKey.cachedItem(appsEntryId()) + return account.postbox.combinedView(keys: [key]) + |> mapToSignal { views -> Signal<[EnginePeer.Id]?, NoError> in + guard let cachedChannels = (views.views[key] as? CachedItemView)?.value?.get(CachedRecommendedChannels.self), !cachedChannels.peerIds.isEmpty else { + return .single(nil) + } + return .single(cachedChannels.peerIds) + } +} + func _internal_recommendedChannels(account: Account, peerId: EnginePeer.Id?) -> Signal { let key = PostboxViewKey.cachedItem(entryId(peerId: peerId)) return account.postbox.combinedView(keys: [key]) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift index 28598bd218a..b81478c4b7b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift @@ -12,6 +12,10 @@ func cachedRecentPeersEntryId() -> ItemCacheEntryId { return ItemCacheEntryId(collectionId: 101, key: CachedRecentPeers.cacheKey()) } +func cachedRecentAppsEntryId() -> ItemCacheEntryId { + return ItemCacheEntryId(collectionId: 102, key: CachedRecentPeers.cacheKey()) +} + public func _internal_recentPeers(accountPeerId: EnginePeer.Id, postbox: Postbox) -> Signal { let key = PostboxViewKey.cachedItem(cachedRecentPeersEntryId()) return postbox.combinedView(keys: [key]) @@ -248,3 +252,80 @@ func _internal_removeRecentlyUsedInlineBot(account: Account, peerId: PeerId) -> } } |> switchToLatest } + +public func _internal_recentApps(accountPeerId: PeerId, postbox: Postbox) -> Signal<[EnginePeer.Id], NoError> { + let key = PostboxViewKey.cachedItem(cachedRecentAppsEntryId()) + return postbox.combinedView(keys: [key]) + |> mapToSignal { views -> Signal<[EnginePeer.Id], NoError> in + if let value = (views.views[key] as? CachedItemView)?.value?.get(CachedRecentPeers.self) { + return .single(value.ids) + } else { + return .single([]) + } + } +} + +public func _internal_managedUpdatedRecentApps(accountPeerId: PeerId, postbox: Postbox, network: Network) -> Signal { + let key = PostboxViewKey.cachedItem(cachedRecentAppsEntryId()) + let peersEnabled = postbox.combinedView(keys: [key]) + |> map { views -> Bool in + if let value = (views.views[key] as? CachedItemView)?.value?.get(CachedRecentPeers.self) { + return value.enabled + } else { + return true + } + } + |> distinctUntilChanged + + let updateOnce = + network.request(Api.functions.contacts.getTopPeers(flags: 1 << 16, offset: 0, limit: 50, hash: 0)) + |> `catch` { _ -> Signal in + return .complete() + } + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> Void in + switch result { + case let .topPeers(_, _, users): + let parsedPeers = AccumulatedPeers(users: users) + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) + + if let entry = CodableEntry(CachedRecentPeers(enabled: true, ids: users.map { $0.peerId })) { + transaction.putItemCacheEntry(id: cachedRecentAppsEntryId(), entry: entry) + } + case .topPeersNotModified: + break + case .topPeersDisabled: + if let entry = CodableEntry(CachedRecentPeers(enabled: false, ids: [])) { + transaction.putItemCacheEntry(id: cachedRecentAppsEntryId(), entry: entry) + } + } + } + } + + return peersEnabled |> mapToSignal { _ -> Signal in + return updateOnce + } +} + +func _internal_removeRecentlyUsedApp(account: Account, peerId: PeerId) -> Signal { + return account.postbox.transaction { transaction -> Signal in + if let entry = transaction.retrieveItemCacheEntry(id: cachedRecentAppsEntryId()), let recentPeers = entry.get(CachedRecentPeers.self) { + let updatedRecentPeers = CachedRecentPeers(enabled: recentPeers.enabled, ids: recentPeers.ids.filter({ $0 != peerId })) + if let updatedEntry = CodableEntry(updatedRecentPeers) { + transaction.putItemCacheEntry(id: cachedRecentAppsEntryId(), entry: updatedEntry) + } + } + + if let peer = transaction.getPeer(peerId), let apiPeer = apiInputPeer(peer) { + return account.network.request(Api.functions.contacts.resetTopPeerRating(category: .topPeerCategoryBotsApp, peer: apiPeer)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } else { + return .complete() + } + } |> switchToLatest +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 6e32017ae92..948d724634a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -600,6 +600,14 @@ public extension TelegramEngine { public func managedUpdatedRecentPeers() -> Signal { return _internal_managedUpdatedRecentPeers(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network) } + + public func recentApps() -> Signal<[EnginePeer.Id], NoError> { + return _internal_recentApps(accountPeerId: self.account.peerId, postbox: self.account.postbox) + } + + public func managedUpdatedRecentApps() -> Signal { + return _internal_managedUpdatedRecentApps(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network) + } public func removeRecentPeer(peerId: PeerId) -> Signal { return _internal_removeRecentPeer(account: self.account, peerId: peerId) @@ -625,6 +633,10 @@ public extension TelegramEngine { public func removeRecentlyUsedInlineBot(peerId: PeerId) -> Signal { return _internal_removeRecentlyUsedInlineBot(account: self.account, peerId: peerId) } + + public func removeRecentlyUsedApp(peerId: PeerId) -> Signal { + return _internal_removeRecentlyUsedApp(account: self.account, peerId: peerId) + } public func uploadedPeerPhoto(resource: MediaResource) -> Signal { return _internal_uploadedPeerPhoto(postbox: self.account.postbox, network: self.account.network, resource: resource) @@ -1423,10 +1435,18 @@ public extension TelegramEngine { return _internal_requestRecommendedChannels(account: self.account, peerId: peerId, forceUpdate: forceUpdate) } + public func recommendedAppPeerIds() -> Signal<[EnginePeer.Id]?, NoError> { + return _internal_recommendedAppPeerIds(account: self.account) + } + public func requestGlobalRecommendedChannelsIfNeeded() -> Signal { return _internal_requestRecommendedChannels(account: self.account, peerId: nil, forceUpdate: false) } + public func requestRecommendedAppsIfNeeded() -> Signal { + return _internal_requestRecommendedApps(account: self.account, forceUpdate: false) + } + public func isPremiumRequiredToContact(_ peerIds: [EnginePeer.Id]) -> Signal<[EnginePeer.Id], NoError> { return _internal_updateIsPremiumRequiredToContact(account: self.account, peerIds: peerIds) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 333ae2772aa..8f1c5fa8e2f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -197,6 +197,17 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee editableBotInfo = .single(nil) } + let botPreview: Signal + if let user = maybePeer as? TelegramUser, let botInfo = user.botInfo { + if botInfo.flags.contains(.canEdit) { + botPreview = _internal_requestBotAdminPreview(network: network, peerId: user.id, inputUser: inputUser, language: nil) + } else { + botPreview = _internal_requestBotUserPreview(network: network, peerId: user.id, inputUser: inputUser) + } + } else { + botPreview = .single(nil) + } + var additionalConnectedBots: Signal = .single(nil) if rawPeerId == accountPeerId { additionalConnectedBots = network.request(Api.functions.account.getConnectedBots()) @@ -210,9 +221,10 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee network.request(Api.functions.users.getFullUser(id: inputUser)) |> retryRequest, editableBotInfo, + botPreview, additionalConnectedBots ) - |> mapToSignal { result, editableBotInfo, additionalConnectedBots -> Signal in + |> mapToSignal { result, editableBotInfo, botPreview, additionalConnectedBots -> Signal in return postbox.transaction { transaction -> Bool in switch result { case let .userFull(fullUser, chats, users): @@ -401,6 +413,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedBusinessIntro(mappedBusinessIntro) .withUpdatedBirthday(mappedBirthday) .withUpdatedPersonalChannel(personalChannel) + .withUpdatedBotPreview(botPreview) } }) } @@ -592,7 +605,9 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee if (flags2 & Int32(1 << 14)) != 0 { channelFlags.insert(.paidMediaAllowed) } - + if (flags2 & Int32(1 << 15)) != 0 { + channelFlags.insert(.canViewStarsRevenue) + } let sendAsPeerId = defaultSendAs?.peerId let linkedDiscussionPeerId: PeerId? @@ -823,3 +838,60 @@ extension CachedPeerAutoremoveTimeout.Value { } } } + +func _internal_requestBotAdminPreview(network: Network, peerId: PeerId, inputUser: Api.InputUser, language: String?) -> Signal { + return network.request(Api.functions.bots.getPreviewInfo(bot: inputUser, langCode: language ?? "")) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> CachedUserData.BotPreview? in + guard let result else { + return nil + } + switch result { + case let .previewInfo(media, langCodes): + return CachedUserData.BotPreview( + items: media.compactMap { item -> CachedUserData.BotPreview.Item? in + switch item { + case let .botPreviewMedia(date, media): + let value = textMediaAndExpirationTimerFromApiMedia(media, peerId) + if let media = value.media { + return CachedUserData.BotPreview.Item(media: media, timestamp: date) + } else { + return nil + } + } + }, + alternativeLanguageCodes: langCodes + ) + } + } +} + +func _internal_requestBotUserPreview(network: Network, peerId: PeerId, inputUser: Api.InputUser) -> Signal { + return network.request(Api.functions.bots.getPreviewMedias(bot: inputUser)) + |> map(Optional.init) + |> `catch` { _ -> Signal<[Api.BotPreviewMedia]?, NoError> in + return .single(nil) + } + |> map { result -> CachedUserData.BotPreview? in + guard let result else { + return nil + } + return CachedUserData.BotPreview( + items: result.compactMap { item -> CachedUserData.BotPreview.Item? in + switch item { + case let .botPreviewMedia(date, media): + let value = textMediaAndExpirationTimerFromApiMedia(media, peerId) + if let media = value.media { + return CachedUserData.BotPreview.Item(media: media, timestamp: date) + } else { + return nil + } + } + }, + alternativeLanguageCodes: [] + ) + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift index fb0878a5ae0..6ba71719066 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift @@ -80,7 +80,7 @@ func _internal_uploadSticker(account: Account, peer: Peer, resource: MediaResour var attributes: [Api.DocumentAttribute] = [] attributes.append(.documentAttributeSticker(flags: 0, alt: alt, stickerset: .inputStickerSetEmpty, maskCoords: nil)) if let duration { - attributes.append(.documentAttributeVideo(flags: 0, duration: duration, w: dimensions.width, h: dimensions.height, preloadPrefixSize: nil)) + attributes.append(.documentAttributeVideo(flags: 0, duration: duration, w: dimensions.width, h: dimensions.height, preloadPrefixSize: nil, videoStartTs: nil)) } attributes.append(.documentAttributeImageSize(w: dimensions.width, h: dimensions.height)) return account.network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: file, thumb: thumbnailFile, mimeType: mimeType, attributes: attributes, stickers: nil, ttlSeconds: nil))) @@ -144,7 +144,7 @@ public extension ImportSticker { fileAttributes.append(.FileName(fileName: "sticker.webm")) fileAttributes.append(.Animated) fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) - fileAttributes.append(.Video(duration: self.duration ?? 3.0, size: self.dimensions, flags: [], preloadSize: nil)) + fileAttributes.append(.Video(duration: self.duration ?? 3.0, size: self.dimensions, flags: [], preloadSize: nil, coverTime: nil)) } else if self.mimeType == "application/x-tgsticker" { fileAttributes.append(.FileName(fileName: "sticker.tgs")) fileAttributes.append(.Animated) diff --git a/submodules/TelegramCore/Sources/UpdatePeers.swift b/submodules/TelegramCore/Sources/UpdatePeers.swift index edbf3ffb9aa..86e58b9712e 100644 --- a/submodules/TelegramCore/Sources/UpdatePeers.swift +++ b/submodules/TelegramCore/Sources/UpdatePeers.swift @@ -51,7 +51,7 @@ func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: Accumul if let telegramUser = TelegramUser.merge(transaction.getPeer(user.peerId) as? TelegramUser, rhs: user) { parsedPeers.append(telegramUser) switch user { - case let .user(flags, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, storiesMaxId, _, _): + case let .user(flags, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, storiesMaxId, _, _, _): let isMin = (flags & (1 << 20)) != 0 let storiesUnavailable = (flags2 & (1 << 4)) != 0 @@ -315,7 +315,7 @@ func updatePeerPresences(transaction: Transaction, accountPeerId: PeerId, peerPr parsedPresences[peerId] = presence default: switch user { - case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): let isMin = (flags & (1 << 20)) != 0 if isMin, let _ = transaction.getPeerPresence(peerId: peerId) { } else { @@ -383,7 +383,7 @@ func updateContacts(transaction: Transaction, apiUsers: [Api.User]) { for user in apiUsers { var isContact: Bool? switch user { - case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): if (flags & (1 << 20)) == 0 { isContact = (flags & (1 << 11)) != 0 } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index cfd2add5c02..91c8c70da58 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -45,6 +45,7 @@ public enum PresentationResourceKey: Int32 { case itemListSecondaryCheckIcon case itemListPlusIcon case itemListRoundPlusIcon + case itemListAccentDeleteIcon case itemListDeleteIcon case itemListDeleteIndicatorIcon case itemListReorderIndicatorIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift index 81e5b19064e..081ee55eeaa 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift @@ -69,6 +69,12 @@ public struct PresentationResourcesItemList { }) } + public static func accentDeleteIconImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListAccentDeleteIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemAccentColor) + }) + } + public static func deleteIconImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.itemListDeleteIcon.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemDestructiveColor) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index 2751b6afc43..0f5c23f5bfe 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -89,6 +89,7 @@ public struct PresentationResourcesSettings { public static let business = renderIcon(name: "Settings/Menu/Business", backgroundColors: [UIColor(rgb: 0xA95CE3), UIColor(rgb: 0xF16B80)]) public static let myProfile = renderIcon(name: "Settings/Menu/Profile") public static let reactions = renderIcon(name: "Settings/Menu/Reactions") + public static let balance = renderIcon(name: "Settings/Menu/Balance", scaleFactor: 0.97, backgroundColors: [UIColor(rgb: 0x34c759)]) public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index 9cba0d586f7..a548f9aac18 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -330,7 +330,7 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil return .file(performer) } } - case let .Video(_, _, flags, _): + case let .Video(_, _, flags, _, _): if file.isAnimated { result = .animation } else { diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index cf7fd661960..1816f40ef87 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -235,7 +235,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } else { for attribute in file.attributes { switch attribute { - case let .Video(_, _, flags, _): + case let .Video(_, _, flags, _, _): if flags.contains(.instantRoundVideo) { type = .round } else { @@ -745,6 +745,23 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributes[1] = boldAttributes attributedString = addAttributesToStringWithRanges(strings.Notification_PremiumGift_Sent(compactAuthorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes) } + case let .giftStars(currency, amount, count, _, _, _): + let _ = count + let price = formatCurrencyAmount(amount, currency: currency) + 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" + peerIds = [] + } + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[1] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } case let .topicCreated(title, iconColor, iconFileId): if forForumOverview { let maybeFileId = iconFileId ?? 0 @@ -992,6 +1009,39 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) } } + 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 mutableString = NSMutableAttributedString() + mutableString.append(NSAttributedString(string: patternString, font: titleFont, textColor: primaryTextColor)) + + var range = NSRange(location: NSNotFound, length: 0) + range = (mutableString.string as NSString).range(of: "{amount}") + if range.location != NSNotFound { + if currency == "XTR" { + let amountAttributedString = NSMutableAttributedString(string: "#\(totalAmount)", font: titleBoldFont, textColor: primaryTextColor) + if let range = amountAttributedString.string.range(of: "#") { + amountAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: amountAttributedString.string)) + amountAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: amountAttributedString.string)) + } + mutableString.replaceCharacters(in: range, with: amountAttributedString) + } else { + mutableString.replaceCharacters(in: range, with: NSAttributedString(string: formatCurrencyAmount(totalAmount, currency: currency), font: titleBoldFont, textColor: primaryTextColor)) + } + } + range = (mutableString.string as NSString).range(of: "{name}") + if range.location != NSNotFound { + let peerName = message.peers[peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + mutableString.replaceCharacters(in: range, with: NSAttributedString(string: peerName, font: titleBoldFont, textColor: primaryTextColor)) + mutableString.addAttribute(NSAttributedString.Key(TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: peerId, mention: ""), range: NSMakeRange(range.location, (peerName as NSString).length)) + } + attributedString = mutableString case .unknown: attributedString = nil } diff --git a/submodules/StatisticsUI/Sources/MonetizationUtils.swift b/submodules/TelegramStringFormatting/Sources/TonFormat.swift similarity index 66% rename from submodules/StatisticsUI/Sources/MonetizationUtils.swift rename to submodules/TelegramStringFormatting/Sources/TonFormat.swift index 3635540ff72..c3374307841 100644 --- a/submodules/StatisticsUI/Sources/MonetizationUtils.swift +++ b/submodules/TelegramStringFormatting/Sources/TonFormat.swift @@ -1,21 +1,33 @@ import Foundation import UIKit +import TelegramPresentationData let walletAddressLength: Int = 48 -func formatAddress(_ address: String) -> String { +public func formatTonAddress(_ address: String) -> String { var address = address address.insert("\n", at: address.index(address.startIndex, offsetBy: address.count / 2)) return address } -func formatUsdValue(_ value: Int64, divide: Bool = true, rate: Double) -> String { +public func formatTonUsdValue(_ value: Int64, divide: Bool = true, rate: Double, dateTimeFormat: PresentationDateTimeFormat) -> String { + let decimalSeparator = dateTimeFormat.decimalSeparator let normalizedValue: Double = divide ? Double(value) / 1000000000 : Double(value) - let formattedValue = String(format: "%0.2f", normalizedValue * rate) + var formattedValue = String(format: "%0.2f", normalizedValue * rate) + formattedValue = formattedValue.replacingOccurrences(of: ".", with: decimalSeparator) + if let dotIndex = formattedValue.firstIndex(of: decimalSeparator.first!) { + let integerPartString = formattedValue[.. String { +public func formatTonAmountText(_ value: Int64, decimalSeparator: String, showPlus: Bool = false) -> String { var balanceText = "\(abs(value))" while balanceText.count < 10 { balanceText.insert("0", at: balanceText.startIndex) @@ -48,7 +60,7 @@ func formatBalanceText(_ value: Int64, decimalSeparator: String, showPlus: Bool } private let invalidAddressCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=").inverted -func isValidAddress(_ address: String, exactLength: Bool = false) -> Bool { +public func isValidTonAddress(_ address: String, exactLength: Bool = false) -> Bool { if address.count > walletAddressLength || address.rangeOfCharacter(from: invalidAddressCharacters) != nil { return false } @@ -59,7 +71,7 @@ func isValidAddress(_ address: String, exactLength: Bool = false) -> Bool { } private let amountDelimeterCharacters = CharacterSet(charactersIn: "0123456789-+").inverted -func amountAttributedString(_ string: String, integralFont: UIFont, fractionalFont: UIFont, color: UIColor) -> NSAttributedString { +public func tonAmountAttributedString(_ string: String, integralFont: UIFont, fractionalFont: UIFont, color: UIColor) -> NSAttributedString { let result = NSMutableAttributedString() if let range = string.rangeOfCharacter(from: amountDelimeterCharacters) { let integralPart = String(string[.. TemperatureUnit { + if let cachedTemperatureUnit { + return cachedTemperatureUnit + } + let temperatureFormatter = MeasurementFormatter() + temperatureFormatter.locale = Locale.current + + let fahrenheitMeasurement = Measurement(value: 0, unit: UnitTemperature.fahrenheit) + let fahrenheitString = temperatureFormatter.string(from: fahrenheitMeasurement) + + var temperatureUnit: TemperatureUnit = .celsius + if fahrenheitString.contains("F") || fahrenheitString.contains("Fahrenheit") { + temperatureUnit = .fahrenheit + } + cachedTemperatureUnit = temperatureUnit + return temperatureUnit +} + +public func stringForTemperature(_ value: Double) -> String { + let formatter = MeasurementFormatter() + formatter.locale = Locale.current + formatter.unitStyle = .short + formatter.numberFormatter.maximumFractionDigits = 0 + formatter.unitOptions = .temperatureWithoutUnit + 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 c05b7289b46..248feb61b63 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -486,6 +486,9 @@ swift_library( "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", "//submodules/TelegramUI/Components/Chat/ChatSendStarsScreen", + "//submodules/TelegramUI/Components/MinimizedContainer", + "//submodules/TelegramUI/Components/SpaceWarpView", + "//submodules/TelegramUI/Components/MiniAppListScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift index b85e764df7e..439be5c6bff 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift @@ -194,6 +194,7 @@ private final class AdminUserActionsSheetComponent: Component { let chatPeer: EnginePeer let peers: [RenderedChannelParticipant] let messageCount: Int + let deleteAllMessageCount: Int? let completion: (AdminUserActionsSheet.Result) -> Void init( @@ -201,12 +202,14 @@ private final class AdminUserActionsSheetComponent: Component { chatPeer: EnginePeer, peers: [RenderedChannelParticipant], messageCount: Int, + deleteAllMessageCount: Int?, completion: @escaping (AdminUserActionsSheet.Result) -> Void ) { self.context = context self.chatPeer = chatPeer self.peers = peers self.messageCount = messageCount + self.deleteAllMessageCount = deleteAllMessageCount self.completion = completion } @@ -223,6 +226,9 @@ private final class AdminUserActionsSheetComponent: Component { if lhs.messageCount != rhs.messageCount { return false } + if lhs.deleteAllMessageCount != rhs.deleteAllMessageCount { + return false + } return true } @@ -642,7 +648,7 @@ private final class AdminUserActionsSheetComponent: Component { let sectionId: AnyHashable let selectedPeers: Set let isExpanded: Bool - let title: String + var title: String switch section { case .report: @@ -870,7 +876,14 @@ private final class AdminUserActionsSheetComponent: Component { ))) } - let titleString: String = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(component.messageCount)) + var titleString: String = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(component.messageCount)) + + if let deleteAllMessageCount = component.deleteAllMessageCount { + if self.optionDeleteAllSelectedPeers == Set(component.peers.map(\.peer.id)) { + titleString = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(deleteAllMessageCount)) + } + } + let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( @@ -884,7 +897,9 @@ private final class AdminUserActionsSheetComponent: Component { if titleView.superview == nil { self.navigationBarContainer.addSubview(titleView) } - transition.setFrame(view: titleView, frame: titleFrame) + //transition.setPosition(view: titleView, position: titleFrame.center) + titleView.center = titleFrame.center + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) } let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0)) @@ -1424,10 +1439,10 @@ public class AdminUserActionsSheet: ViewControllerComponentContainer { private var isDismissed: Bool = false - public init(context: AccountContext, chatPeer: EnginePeer, peers: [RenderedChannelParticipant], messageCount: Int, completion: @escaping (Result) -> Void) { + public init(context: AccountContext, chatPeer: EnginePeer, peers: [RenderedChannelParticipant], messageCount: Int, deleteAllMessageCount: Int?, completion: @escaping (Result) -> Void) { self.context = context - super.init(context: context, component: AdminUserActionsSheetComponent(context: context, chatPeer: chatPeer, peers: peers, messageCount: messageCount, completion: completion), navigationBarAppearance: .none) + super.init(context: context, component: AdminUserActionsSheetComponent(context: context, chatPeer: chatPeer, peers: peers, messageCount: messageCount, deleteAllMessageCount: deleteAllMessageCount, completion: completion), navigationBarAppearance: .none) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal diff --git a/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD b/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD index 0a909de49bf..809251cb9e8 100644 --- a/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD +++ b/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/UndoUI", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/NavigationStackComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift index 17359fffd5e..49dccb428ee 100644 --- a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift @@ -13,6 +13,7 @@ import BalancedTextComponent import MultilineTextComponent import ListSectionComponent import ListActionItemComponent +import NavigationStackComponent import ItemListUI import UndoUI import AccountContext @@ -655,297 +656,3 @@ public final class AdsReportScreen: ViewControllerComponentContainer { } } } - - - -private final class NavigationContainer: UIView, UIGestureRecognizerDelegate { - var requestUpdate: ((ComponentTransition) -> Void)? - var requestPop: (() -> Void)? - var transitionFraction: CGFloat = 0.0 - - private var panRecognizer: InteractiveTransitionGestureRecognizer? - - var isNavigationEnabled: Bool = false { - didSet { - self.panRecognizer?.isEnabled = self.isNavigationEnabled - } - } - - init() { - super.init(frame: .zero) - - let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in - guard let strongSelf = self else { - return [] - } - let _ = strongSelf - return [.right] - }) - panRecognizer.delegate = self - self.addGestureRecognizer(panRecognizer) - self.panRecognizer = panRecognizer - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return false - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { - if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer { - return false - } - if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { - return true - } - return false - } - - @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { - switch recognizer.state { - case .began: - self.transitionFraction = 0.0 - case .changed: - let distanceFactor: CGFloat = recognizer.translation(in: self).x / self.bounds.width - let transitionFraction = max(0.0, min(1.0, distanceFactor)) - if self.transitionFraction != transitionFraction { - self.transitionFraction = transitionFraction - self.requestUpdate?(.immediate) - } - case .ended, .cancelled: - let distanceFactor: CGFloat = recognizer.translation(in: self).x / self.bounds.width - let transitionFraction = max(0.0, min(1.0, distanceFactor)) - if transitionFraction > 0.2 { - self.transitionFraction = 0.0 - self.requestPop?() - } else { - self.transitionFraction = 0.0 - self.requestUpdate?(.spring(duration: 0.45)) - } - default: - break - } - } -} - -final class NavigationStackComponent: Component { - public let items: [AnyComponentWithIdentity] - public let requestPop: () -> Void - - public init( - items: [AnyComponentWithIdentity], - requestPop: @escaping () -> Void - ) { - self.items = items - self.requestPop = requestPop - } - - public static func ==(lhs: NavigationStackComponent, rhs: NavigationStackComponent) -> Bool { - if lhs.items != rhs.items { - return false - } - return true - } - - private final class ItemView: UIView { - let contents = ComponentView() - let dimView = UIView() - - override init(frame: CGRect) { - super.init(frame: frame) - - self.dimView.alpha = 0.0 - self.dimView.backgroundColor = UIColor.black.withAlphaComponent(0.2) - self.dimView.isUserInteractionEnabled = false - self.addSubview(self.dimView) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - } - - private struct ReadyItem { - var index: Int - var itemId: AnyHashable - var itemView: ItemView - var itemTransition: ComponentTransition - var itemSize: CGSize - - init(index: Int, itemId: AnyHashable, itemView: ItemView, itemTransition: ComponentTransition, itemSize: CGSize) { - self.index = index - self.itemId = itemId - self.itemView = itemView - self.itemTransition = itemTransition - self.itemSize = itemSize - } - } - - public final class View: UIView { - private var itemViews: [AnyHashable: ItemView] = [:] - private let navigationContainer = NavigationContainer() - - private var component: NavigationStackComponent? - private var state: EmptyComponentState? - - public override init(frame: CGRect) { - super.init(frame: CGRect()) - - self.addSubview(self.navigationContainer) - - self.navigationContainer.requestUpdate = { [weak self] transition in - guard let self else { - return - } - self.state?.updated(transition: transition) - } - - self.navigationContainer.requestPop = { [weak self] in - guard let self else { - return - } - self.component?.requestPop() - } - } - - required public init?(coder: NSCoder) { - preconditionFailure() - } - - func update(component: NavigationStackComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.component = component - self.state = state - - let navigationTransitionFraction = self.navigationContainer.transitionFraction - self.navigationContainer.isNavigationEnabled = component.items.count > 1 - - var validItemIds: [AnyHashable] = [] - - - var readyItems: [ReadyItem] = [] - for i in 0 ..< component.items.count { - let item = component.items[i] - let itemId = item.id - validItemIds.append(itemId) - - let itemView: ItemView - var itemTransition = transition - if let current = self.itemViews[itemId] { - itemView = current - } else { - itemTransition = itemTransition.withAnimation(.none) - itemView = ItemView() - self.itemViews[itemId] = itemView - itemView.contents.parentState = state - } - - let itemSize = itemView.contents.update( - transition: itemTransition, - component: item.component, - environment: { environment[ChildEnvironment.self] }, - containerSize: CGSize(width: availableSize.width, height: availableSize.height) - ) - - readyItems.append(ReadyItem( - index: i, - itemId: itemId, - itemView: itemView, - itemTransition: itemTransition, - itemSize: itemSize - )) - } - - let sortedItems = readyItems.sorted(by: { $0.index < $1.index }) - for readyItem in sortedItems { - let transitionFraction: CGFloat - let alphaTransitionFraction: CGFloat - if readyItem.index == readyItems.count - 1 { - transitionFraction = navigationTransitionFraction - alphaTransitionFraction = 1.0 - } else if readyItem.index == readyItems.count - 2 { - transitionFraction = navigationTransitionFraction - 1.0 - alphaTransitionFraction = navigationTransitionFraction - } else { - transitionFraction = 0.0 - alphaTransitionFraction = 0.0 - } - - let transitionOffset: CGFloat - if readyItem.index == readyItems.count - 1 { - transitionOffset = readyItem.itemSize.width * transitionFraction - } else { - transitionOffset = readyItem.itemSize.width / 3.0 * transitionFraction - } - - let itemFrame = CGRect(origin: CGPoint(x: transitionOffset, y: 0.0), size: readyItem.itemSize) - - let itemBounds = CGRect(origin: .zero, size: itemFrame.size) - if let itemComponentView = readyItem.itemView.contents.view { - var isAdded = false - if itemComponentView.superview == nil { - isAdded = true - - readyItem.itemView.insertSubview(itemComponentView, at: 0) - self.navigationContainer.addSubview(readyItem.itemView) - } - readyItem.itemTransition.setFrame(view: readyItem.itemView, frame: itemFrame) - readyItem.itemTransition.setFrame(view: itemComponentView, frame: itemBounds) - readyItem.itemTransition.setFrame(view: readyItem.itemView.dimView, frame: CGRect(origin: .zero, size: availableSize)) - readyItem.itemTransition.setAlpha(view: readyItem.itemView.dimView, alpha: 1.0 - alphaTransitionFraction) - - if readyItem.index > 0 && isAdded { - transition.animatePosition(view: itemComponentView, from: CGPoint(x: itemFrame.width, y: 0.0), to: .zero, additive: true, completion: nil) - } - } - } - - let lastHeight = sortedItems.last?.itemSize.height ?? 0.0 - let previousHeight: CGFloat - if sortedItems.count > 1 { - previousHeight = sortedItems[sortedItems.count - 2].itemSize.height - } else { - previousHeight = lastHeight - } - let contentHeight = lastHeight * (1.0 - navigationTransitionFraction) + previousHeight * navigationTransitionFraction - - var removedItemIds: [AnyHashable] = [] - for (id, _) in self.itemViews { - if !validItemIds.contains(id) { - removedItemIds.append(id) - } - } - for id in removedItemIds { - guard let itemView = self.itemViews[id] else { - continue - } - if let itemComponeentView = itemView.contents.view { - var position = itemComponeentView.center - position.x += itemComponeentView.bounds.width - transition.setPosition(view: itemComponeentView, position: position, completion: { _ in - itemView.removeFromSuperview() - self.itemViews.removeValue(forKey: id) - }) - } else { - itemView.removeFromSuperview() - self.itemViews.removeValue(forKey: id) - } - } - - let contentSize = CGSize(width: availableSize.width, height: contentHeight) - self.navigationContainer.frame = CGRect(origin: .zero, size: contentSize) - - return contentSize - } - } - - 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, state: state, environment: environment, transition: transition) - } -} diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 55084110b92..8c63e8750b4 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -847,7 +847,12 @@ private final class CameraScreenComponent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) .scale(1.5 - component.cameraState.flashTintSize * 0.5) .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) + .disappear(ComponentTransition.Disappear({ view, transition, completion in + view.superview?.sendSubviewToBack(view) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + })) ) if !state.isTakingPhoto { @@ -1391,15 +1396,18 @@ public class CameraScreen: ViewController { public weak var destinationView: UIView? public let destinationRect: CGRect public let destinationCornerRadius: CGFloat + public let completion: (() -> Void)? public init( destinationView: UIView, destinationRect: CGRect, - destinationCornerRadius: CGFloat + destinationCornerRadius: CGFloat, + completion: (() -> Void)? = nil ) { self.destinationView = destinationView self.destinationRect = destinationRect self.destinationCornerRadius = destinationCornerRadius + self.completion = completion } } @@ -1535,6 +1543,9 @@ public class CameraScreen: ViewController { isDualCameraEnabled = isDualCameraEnabledValue.boolValue } } + if case .sticker = controller.mode { + isDualCameraEnabled = false + } var dualCameraPosition: PIPPosition = .topRight if let dualCameraPositionValue = UserDefaults.standard.object(forKey: "TelegramStoryCameraDualPosition") as? NSNumber { @@ -2177,9 +2188,12 @@ public class CameraScreen: ViewController { let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view) let targetScale = destinationLocalFrame.width / self.previewContainerView.frame.width + let transitionOutCompletion = transitionOut.completion + if case .story = controller.mode { self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: destinationLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in completion() + transitionOutCompletion?() }) self.previewContainerView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) @@ -2203,6 +2217,7 @@ public class CameraScreen: ViewController { self.mainPreviewAnimationWrapperView.center = destinationInnerFrame.center self.mainPreviewAnimationWrapperView.layer.animatePosition(from: initialCenter, to: self.mainPreviewAnimationWrapperView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in completion() + transitionOutCompletion?() }) var targetBounds = self.mainPreviewView.bounds @@ -2974,7 +2989,7 @@ public class CameraScreen: ViewController { if let current = self.galleryController { controller = current } else { - controller = self.context.sharedContext.makeStoryMediaPickerScreen(context: self.context, getSourceRect: { [weak self] in + controller = self.context.sharedContext.makeStoryMediaPickerScreen(context: self.context, isDark: true, getSourceRect: { [weak self] in if let self { if let galleryButton = self.node.componentHost.findTaggedView(tag: galleryButtonTag) { return galleryButton.convert(galleryButton.bounds, to: self.view).offsetBy(dx: 0.0, dy: -15.0) diff --git a/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift b/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift index d32cd4adf89..67891754b99 100644 --- a/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift @@ -172,7 +172,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont imageDimensions = externalReference.content?.dimensions?.cgSize if let content = externalReference.content, externalReference.type == "gif", let thumbnailResource = imageResource , let dimensions = content.dimensions { - videoFileReference = .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil)])) + videoFileReference = .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)])) imageResource = nil } case let .internalReference(internalReference): diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/BUILD index 1a9fe9c855a..cc6afb46a20 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/ReactionSelectionNode", "//submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem", "//submodules/PremiumUI", + "//submodules/TelegramUI/Components/LottieComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index d4388e4be23..7eccdcf765f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -22,6 +22,7 @@ import ReactionSelectionNode import ChatMediaInputStickerGridItem import UndoUI import PremiumUI +import LottieComponent private protocol ChatEmptyNodeContent { func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize @@ -1203,7 +1204,7 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE private let interaction: ChatPanelInterfaceInteraction? private let iconBackground: SimpleLayer - private let icon: UIImageView + private let icon = ComponentView() private let text = ComponentView() private let buttonTitle = ComponentView() private let button: HighlightTrackingButton @@ -1219,8 +1220,7 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE self.interaction = interaction self.iconBackground = SimpleLayer() - self.icon = UIImageView(image: UIImage(bundleImageName: "Chat/Empty Chat/PremiumRequiredIcon")?.withRenderingMode(.alwaysTemplate)) - + self.button = HighlightTrackingButton() self.button.clipsToBounds = true @@ -1230,7 +1230,6 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE super.init() self.layer.addSublayer(self.iconBackground) - self.view.addSubview(self.icon) if !self.isPremiumDisabled { self.view.addSubview(self.button) @@ -1330,11 +1329,28 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE contentsHeight += iconBackgroundSize contentsHeight += iconTextSpacing - self.icon.tintColor = serviceColor.primaryText - if let image = self.icon.image { - transition.updateFrame(view: self.icon, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floor((iconBackgroundFrame.width - image.size.width) * 0.5), y: iconBackgroundFrame.minY + floor((iconBackgroundFrame.height - image.size.height) * 0.5)), size: image.size)) + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent( + LottieComponent( + content: LottieComponent.AppBundleContent(name: "PremiumRequired"), + color: serviceColor.primaryText, + size: CGSize(width: 120.0, height: 120.0), + loop: true + ) + ), + environment: {}, + containerSize: CGSize(width: maxWidth - sideInset * 2.0, height: 500.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floor((iconBackgroundFrame.width - iconSize.width) * 0.5), y: iconBackgroundFrame.minY + floor((iconBackgroundFrame.height - iconSize.height) * 0.5)), size: iconSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + self.view.addSubview(iconView) + } + iconView.frame = iconFrame } - + let textFrame = CGRect(origin: CGPoint(x: floor((contentsWidth - textSize.width) * 0.5), y: contentsHeight), size: textSize) if let textView = self.text.view { if textView.superview == nil { diff --git a/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift index 809deb1ba64..79be541f9a3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift +++ b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift @@ -43,7 +43,7 @@ public struct ChatMessageEntryAttributes: Equatable { public enum ChatHistoryEntry: Identifiable, Comparable { case MessageEntry(Message, ChatPresentationData, Bool, MessageHistoryEntryLocation?, ChatHistoryMessageSelection, ChatMessageEntryAttributes) - case MessageGroupEntry(MessageGroupInfo, [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)], ChatPresentationData) + case MessageGroupEntry(Int64, [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)], ChatPresentationData) case UnreadEntry(MessageIndex, ChatPresentationData) case ReplyCountEntry(MessageIndex, Bool, Int, ChatPresentationData) case ChatInfoEntry(String, String, TelegramMediaImage?, TelegramMediaFile?, ChatPresentationData) @@ -63,7 +63,7 @@ public enum ChatHistoryEntry: Identifiable, Comparable { } return UInt64(message.stableId) | ((type << 40)) case let .MessageGroupEntry(groupInfo, _, _): - return UInt64(groupInfo.stableId) | ((UInt64(2) << 40)) + return UInt64(bitPattern: groupInfo) | ((UInt64(2) << 40)) case .UnreadEntry: return UInt64(4) << 40 case .ReplyCountEntry: diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index 802c7ee1557..0206dacbb74 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -753,14 +753,14 @@ public final class ChatInlineSearchResultsListComponent: Component { if let forwardInfo = message.forwardInfo { effectiveAuthor = forwardInfo.author.flatMap(EnginePeer.init) if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature { - effectiveAuthor = EnginePeer(TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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)) + effectiveAuthor = EnginePeer(TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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)) } } if let sourceAuthorInfo = message._asMessage().sourceAuthorInfo { if let originalAuthor = sourceAuthorInfo.originalAuthor, let peer = message.peers[originalAuthor] { effectiveAuthor = EnginePeer(peer) } else if let authorSignature = sourceAuthorInfo.originalAuthorName { - effectiveAuthor = EnginePeer(TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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)) + effectiveAuthor = EnginePeer(TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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)) } } if effectiveAuthor == nil { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift index d0066f268fb..feaacffdb13 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift @@ -272,7 +272,7 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.mediaBackgroundNode.image = backgroundImage if let image = image, let video = image.videoRepresentations.last, let id = image.id?.id { - let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: image.representations, videoThumbnails: [], immediateThumbnailData: image.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil)])) + let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: image.representations, videoThumbnails: [], immediateThumbnailData: image.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), userLocation: .peer(item.message.id.peerId), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) if videoContent.id != strongSelf.videoContent?.id { let mediaManager = item.context.sharedContext.mediaManager diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift index 730934d938b..b52bbbe9782 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift @@ -179,7 +179,17 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { case .text: iconImage = incoming ? graphics.chatBubbleActionButtonIncomingMessageIconImage : graphics.chatBubbleActionButtonOutgoingMessageIconImage case let .url(value): - if isTelegramMeLink(value), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: value), case .peer(_, .appStart) = internalUrl { + var isApp = false + if isTelegramMeLink(value), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: value) { + if case .peer(_, .appStart) = internalUrl { + isApp = true + } else if case .peer(_, .attachBotStart) = internalUrl { + isApp = true + } else if case .startAttach = internalUrl { + isApp = true + } + } + if isApp { iconImage = incoming ? graphics.chatBubbleActionButtonIncomingWebAppIconImage : graphics.chatBubbleActionButtonOutgoingWebAppIconImage } else if value.lowercased().contains("?startgroup=") { iconImage = incoming ? graphics.chatBubbleActionButtonIncomingAddToChatIconImage : graphics.chatBubbleActionButtonOutgoingAddToChatIconImage diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 9a4942392f6..430efe041f6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -402,7 +402,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.animationNode = animationNode } } else { - let animationNode = DefaultAnimatedStickerNodeImpl(useMetalCache: item.context.sharedContext.immediateExperimentalUISettings.acceleratedStickers) + let animationNode = DefaultAnimatedStickerNodeImpl(useMetalCache: false) animationNode.started = { [weak self] in if let strongSelf = self { strongSelf.imageNode.alpha = 0.0 @@ -1842,7 +1842,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } } else if case .tap = gesture { - item.controllerInteraction.clickThroughMessage() + item.controllerInteraction.clickThroughMessage(self.view, location) } else if case .doubleTap = gesture { // MARK: Nicegram HideReactions, account added if canAddMessageReactions(message: item.message, account: item.context.account) { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 4c4c6237fed..4ee70dcddd5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -234,7 +234,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { if let peer = forwardInfo.author { author = peer } else if let authorSignature = forwardInfo.authorSignature { - author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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) + author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index d67e2f811c9..530eec60ee9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -226,6 +226,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.append((message, ChatMessageCallBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .giftPremium = action.action { result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + } else if case .giftStars = action.action { + result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .suggestedProfilePhoto = action.action { result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .setChatWallpaper = action.action { @@ -1526,11 +1528,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let sourceAuthorInfo, let originalAuthorId = sourceAuthorInfo.originalAuthor, let peer = item.message.peers[originalAuthorId] { effectiveAuthor = peer } else if let sourceAuthorInfo, let originalAuthorName = sourceAuthorInfo.originalAuthorName { - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(originalAuthorName.persistentHashValue % 32))), accessHash: nil, firstName: originalAuthorName, 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) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(originalAuthorName.persistentHashValue % 32))), accessHash: nil, firstName: originalAuthorName, 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) } else { ignoreForward = true if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature { - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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) } } } @@ -1547,7 +1549,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI displayAuthorInfo = !mergedTop.merged && incoming } else if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature { ignoreForward = true - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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) displayAuthorInfo = !mergedTop.merged && incoming } else if let _ = item.content.firstMessage.adAttribute, let author = item.content.firstMessage.author { ignoreForward = true @@ -4763,7 +4765,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } } else if case .tap = gesture { - item.controllerInteraction.clickThroughMessage() + item.controllerInteraction.clickThroughMessage(self.view, location) } else if case .doubleTap = gesture { // MARK: Nicegram HideReactions, account added if canAddMessageReactions(message: item.message, account: item.context.account) { @@ -4938,25 +4940,31 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI // NG_TODO: Fix code/pre copy if let forwardInfoNode = self.forwardInfoNode, forwardInfoNode.frame.contains(location) { if let item = self.item, let forwardInfo = item.message.forwardInfo { - let performAction: () -> Void = { + let performAction: () -> Void = { [weak forwardInfoNode] in if let sourceMessageId = forwardInfo.sourceMessageId { if let channel = forwardInfo.author as? TelegramChannel, channel.addressName == nil { if case let .broadcast(info) = channel.info, info.flags.contains(.hasDiscussionGroup) { } else if case .member = channel.participationStatus { } else if !item.message.id.peerId.isReplies { - item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, false, forwardInfoNode, nil) + if let forwardInfoNode { + item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, false, forwardInfoNode, nil) + } return } } - item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil)) + if let forwardInfoNode { + item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil, progress: forwardInfoNode.makeActivate()?())) + } } else if let peer = forwardInfo.source ?? forwardInfo.author { item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info(nil) : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) } else if let _ = forwardInfo.authorSignature { - var subRect: CGRect? - if let textNode = forwardInfoNode.nameNode { - subRect = textNode.frame + if let forwardInfoNode { + var subRect: CGRect? + if let textNode = forwardInfoNode.nameNode { + subRect = textNode.frame + } + item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, false, forwardInfoNode, subRect) } - item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, false, forwardInfoNode, subRect) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/BUILD index a0993e63e46..d11e2dccc13 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/BUILD @@ -25,6 +25,7 @@ swift_library( "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/TextLoadingEffect", "//submodules/AvatarNode", ], visibility = [ diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift index 8d79dea5e71..33121fb3379 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift @@ -8,6 +8,8 @@ import TelegramPresentationData import LocalizedPeerData import AccountContext import AvatarNode +import TextLoadingEffect +import SwiftSignalKit public enum ChatMessageForwardInfoType: Equatable { case bubble(incoming: Bool) @@ -85,6 +87,10 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { private var highlightColor: UIColor? private var linkHighlightingNode: LinkHighlightingNode? + private var hasLinkProgress: Bool = false + private var linkProgressView: TextLoadingEffectView? + private var linkProgressDisposable: Disposable? + private var previousPeer: Peer? public var openPsa: ((String, ASDisplayNode) -> Void)? @@ -93,6 +99,10 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { super.init() } + deinit { + self.linkProgressDisposable?.dispose() + } + public func hasAction(at point: CGPoint) -> Bool { if let infoNode = self.infoNode, infoNode.frame.contains(point) { return true @@ -172,7 +182,6 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { if isHighlighted, !initialRects.isEmpty, let highlightColor = self.highlightColor { let rects = initialRects - let linkHighlightingNode: LinkHighlightingNode if let current = self.linkHighlightingNode { linkHighlightingNode = current @@ -191,6 +200,85 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { } } + public func makeActivate() -> (() -> Promise?)? { + return { [weak self] in + guard let self else { + return nil + } + + let promise = Promise() + self.linkProgressDisposable?.dispose() + + if self.hasLinkProgress { + self.hasLinkProgress = false + self.updateLinkProgressState() + } + + self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in + guard let self else { + return + } + if self.hasLinkProgress != value { + self.hasLinkProgress = value + self.updateLinkProgressState() + } + }) + + return promise + } + } + + private func updateLinkProgressState() { + guard let highlightColor = self.highlightColor else { + return + } + + if self.hasLinkProgress, let titleNode = self.titleNode, let nameNode = self.nameNode { + var initialRects: [CGRect] = [] + let addRects: (TextNode, CGPoint, CGFloat) -> Void = { textNode, offset, additionalWidth in + guard let cachedLayout = textNode.cachedLayout else { + return + } + for rect in cachedLayout.linesRects() { + var rect = rect + rect.size.width += rect.origin.x + additionalWidth + rect.origin.x = 0.0 + initialRects.append(rect.offsetBy(dx: offset.x, dy: offset.y)) + } + } + + let offsetY: CGFloat = -12.0 + if let titleNode = self.titleNode { + addRects(titleNode, CGPoint(x: titleNode.frame.minX, y: offsetY + titleNode.frame.minY), 0.0) + + if let nameNode = self.nameNode { + addRects(nameNode, CGPoint(x: titleNode.frame.minX, y: offsetY + nameNode.frame.minY), nameNode.frame.minX - titleNode.frame.minX) + } + } + + let linkProgressView: TextLoadingEffectView + if let current = self.linkProgressView { + linkProgressView = current + } else { + linkProgressView = TextLoadingEffectView(frame: CGRect()) + self.linkProgressView = linkProgressView + self.view.addSubview(linkProgressView) + } + linkProgressView.frame = titleNode.frame + + let progressColor: UIColor = highlightColor + + linkProgressView.update(color: progressColor, size: CGRectUnion(titleNode.frame, nameNode.frame).size, rects: initialRects) + } else { + if let linkProgressView = self.linkProgressView { + self.linkProgressView = nil + linkProgressView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak linkProgressView] _ in + linkProgressView?.removeFromSuperview() + }) + } + } + } + public static func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer?, _ authorName: String?, _ psaType: String?, _ storyData: StoryData?, _ constrainedSize: CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode) { let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode) let nameNodeLayout = TextNode.asyncLayout(maybeNode?.nameNode) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 252c593418b..43f5fe28b21 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -235,6 +235,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in var giftSize = CGSize(width: 220.0, height: 240.0) + let incoming = item.message.effectivelyIncoming(item.context.account.peerId) + let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: EngineMessage(item.message), accountPeerId: item.context.account.peerId) let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText @@ -252,6 +254,20 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { case let .giftPremium(_, _, monthsValue, _, _): months = monthsValue text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string + case let .giftStars(_, _, count, _, _, _): + if count <= 1000 { + months = 3 + } else if count < 2500 { + months = 6 + } else { + months = 12 + } + var peerName = "" + if let peer = item.message.peers[item.message.id.peerId] { + peerName = EnginePeer(peer).compactDisplayTitle + } + title = item.presentationData.strings.Notification_StarsGift_Title(Int32(count)) + text = incoming ? item.presentationData.strings.Notification_StarsGift_Subtitle : item.presentationData.strings.Notification_StarsGift_SubtitleYou(peerName).string case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _): if channelId == nil { months = monthsValue diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index 59b4284e66b..f9c0c49728d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -961,7 +961,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco break } } else if case .tap = gesture { - self.item?.controllerInteraction.clickThroughMessage() + self.item?.controllerInteraction.clickThroughMessage(self.view, location) } } default: diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index ecb5d216f45..079ea26cf62 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -784,7 +784,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { let messageTheme = arguments.incoming ? arguments.presentationData.theme.theme.chat.message.incoming : arguments.presentationData.theme.theme.chat.message.outgoing let isInstantVideo = arguments.file.isInstantVideo for attribute in arguments.file.attributes { - if case let .Video(videoDuration, _, flags, _) = attribute, flags.contains(.instantRoundVideo) { + if case let .Video(videoDuration, _, flags, _, _) = attribute, flags.contains(.instantRoundVideo) { isAudio = true isVoice = true @@ -1706,7 +1706,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { var isVoice = false var audioDuration: Int32? for attribute in file.attributes { - if case let .Video(duration, _, flags, _) = attribute, flags.contains(.instantRoundVideo) { + if case let .Video(duration, _, flags, _, _) = attribute, flags.contains(.instantRoundVideo) { isAudio = true isVoice = true audioDuration = Int32(duration) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index d119a50ed4e..5dcd3a1f815 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -1586,7 +1586,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { return } - self.item?.controllerInteraction.clickThroughMessage() + self.item?.controllerInteraction.clickThroughMessage(self.view, location) case .longTap, .doubleTap, .secondaryTap: break case .hold: diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 6f3e5a04347..4571bc10740 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -1548,7 +1548,14 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } - let arguments = TransformImageArguments(corners: corners, imageSize: drawingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: isInlinePlayableVideo ? .fill(.black) : .blurBackground, emptyColor: emptyColor, custom: patternArguments) + var videoCorners = corners + var imageCorners = corners + if let file = media as? TelegramMediaFile, file.isInstantVideo { + videoCorners = ImageCorners(radius: boundingSize.width / 2.0) + imageCorners = ImageCorners(radius: 0.0) + } + + let arguments = TransformImageArguments(corners: imageCorners, imageSize: drawingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: isInlinePlayableVideo ? .fill(.black) : .blurBackground, emptyColor: emptyColor, custom: patternArguments) let imageFrame = CGRect(origin: CGPoint(x: -arguments.insets.left, y: -arguments.insets.top), size: arguments.drawingSize).ensuredValid let cleanImageFrame = CGRect(origin: imageFrame.origin, size: CGSize(width: imageFrame.width - arguments.corners.extendedEdges.right, height: imageFrame.height)) @@ -1568,7 +1575,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr strongSelf.automaticPlayback = automaticPlayback strongSelf.automaticDownload = automaticDownload strongSelf.preferredStoryHighQuality = associatedData.preferredStoryHighQuality - + if let previousArguments = strongSelf.currentImageArguments { if previousArguments.imageSize == arguments.imageSize { strongSelf.pinchContainerNode.frame = imageFrame @@ -1621,7 +1628,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr statusFrame.origin.y = floor(imageFrame.height / 2.0 - statusFrame.height / 2.0) statusNode.frame = statusFrame } - + var updatedVideoNodeReadySignal: Signal? var updatedPlayerStatusSignal: Signal? if let currentReplaceVideoNode = replaceVideoNode { @@ -1633,7 +1640,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } if currentReplaceVideoNode, let updatedVideoFile = updateVideoFile { - let decoration = ChatBubbleVideoDecoration(corners: arguments.corners, nativeSize: nativeSize, contentMode: contentMode.bubbleVideoDecorationContentMode, backgroundColor: arguments.emptyColor ?? .black) + let decoration = ChatBubbleVideoDecoration(corners: videoCorners, nativeSize: nativeSize, contentMode: contentMode.bubbleVideoDecorationContentMode, backgroundColor: arguments.emptyColor ?? .black) strongSelf.videoNodeDecoration = decoration let mediaManager = context.sharedContext.mediaManager @@ -1705,10 +1712,17 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr if message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }), strongSelf.extendedMediaOverlayNode == nil { strongSelf.internallyVisible = false } - + if let videoNode = strongSelf.videoNode { - if !(replaceVideoNode ?? false), let decoration = videoNode.decoration as? ChatBubbleVideoDecoration, decoration.corners != corners { - decoration.updateCorners(corners) + if !(replaceVideoNode ?? false), let decoration = videoNode.decoration as? ChatBubbleVideoDecoration, decoration.corners != videoCorners { + decoration.updateCorners(videoCorners) + } + + if !videoCorners.isEmpty && imageCorners.isEmpty { + strongSelf.imageNode.clipsToBounds = true + strongSelf.imageNode.cornerRadius = videoCorners.topLeft.radius + } else { + strongSelf.imageNode.cornerRadius = 0.0 } videoNode.updateLayout(size: arguments.drawingSize, transition: .immediate) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 605aab862d9..91e3e2c5378 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -27,7 +27,7 @@ private func mediaMergeableStyle(_ media: Media) -> ChatMessageMerge { switch attribute { case .Sticker: return .semanticallyMerged - case let .Video(_, _, flags, _): + case let .Video(_, _, flags, _, _): if flags.contains(.instantRoundVideo) { return .none } @@ -288,14 +288,14 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible if let forwardInfo = content.firstMessage.forwardInfo { effectiveAuthor = forwardInfo.author if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature { - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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) } } if let sourceAuthorInfo = content.firstMessage.sourceAuthorInfo { if let originalAuthor = sourceAuthorInfo.originalAuthor, let peer = content.firstMessage.peers[originalAuthor] { effectiveAuthor = peer } else if let authorSignature = sourceAuthorInfo.originalAuthorName { - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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) } } displayAuthorInfo = incoming && effectiveAuthor != nil @@ -430,7 +430,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible viewClassName = ChatMessageStickerItemNode.self } break loop - case let .Video(_, _, flags, _): + case let .Video(_, _, flags, _, _): if flags.contains(.instantRoundVideo) { viewClassName = ChatMessageBubbleItemNode.self break loop diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index b8ef21ddf06..ea4dc7c4800 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -203,7 +203,7 @@ public final class ChatMessageAccessibilityData { text = item.presentationData.strings.VoiceOver_Chat_MusicTitle(title, performer).string text.append(item.presentationData.strings.VoiceOver_Chat_Duration(durationString).string) } - case let .Video(duration, _, flags, _): + case let .Video(duration, _, flags, _, _): isSpecialFile = true if isSelected == nil { hint = item.presentationData.strings.VoiceOver_Chat_PlayHint diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift index d3187ba59bd..7f8236c6ee0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift @@ -218,7 +218,7 @@ public class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleCont } if let photo = photo, let video = photo.videoRepresentations.last, let id = photo.id?.id { - let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil)])) + let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), userLocation: .peer(item.message.id.peerId), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) if videoContent.id != strongSelf.videoContent?.id { let mediaManager = item.context.sharedContext.mediaManager diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift index 2a69aa8db64..7c350d7d90f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift @@ -209,7 +209,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { if let peer = forwardInfo.author { author = peer } else if let authorSignature = forwardInfo.authorSignature { - author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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) + author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, 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/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index 31cc3353a60..926eb1419ca 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -1406,7 +1406,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } } } else if case .tap = gesture { - self.item?.controllerInteraction.clickThroughMessage() + self.item?.controllerInteraction.clickThroughMessage(self.view, location) } } default: diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index 9d73bee8ebb..a869fe15437 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -395,7 +395,11 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent actionTitle = item.presentationData.strings.Conversation_ViewMessage } case "telegram_user": - actionTitle = item.presentationData.strings.Conversation_UserSendMessage + if webpage.displayUrl.contains("?profile") { + actionTitle = item.presentationData.strings.Conversation_OpenProfile + } else { + actionTitle = item.presentationData.strings.Conversation_UserSendMessage + } case "telegram_channel_request": actionTitle = item.presentationData.strings.Conversation_RequestToJoinChannel case "telegram_chat_request", "telegram_megagroup_request": diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD index 18a1daddd63..59202ab77ed 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD @@ -51,6 +51,7 @@ swift_library( "//submodules/TextFormat", "//submodules/CounterControllerTitleView", "//submodules/TelegramUI/Components/AdminUserActionsSheet", + "//submodules/BrowserUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 7bdc5a1bdec..32d3549ad1f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -306,7 +306,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if let context = self?.context, let navigationController = self?.getNavigationController() { let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .always).startStandalone() } - }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false + }, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { [weak self] messageId, _, _, _ in guard let self else { return @@ -320,7 +320,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self?.openUrl(url.url) }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { - strongSelf.context.sharedContext.openChatInstantPage(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController) + if let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType) { + navigationController.pushViewController(controller) + } } }, openWallpaper: { [weak self] message in if let strongSelf = self{ @@ -1179,11 +1181,11 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case let .channelMessage(peer, messageId, timecode): if let navigationController = strongSelf.getNavigationController() { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode))) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode, setupReply: false))) } case let .replyThreadMessage(replyThreadMessage, messageId): if let navigationController = strongSelf.getNavigationController() { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(replyThreadMessage), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil))) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(replyThreadMessage), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) } case let .replyThread(messageId): if let navigationController = strongSelf.getNavigationController() { @@ -1223,8 +1225,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } case .chatFolder: break - case let .instantView(webpage, anchor): - strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .peer(strongSelf.peer.id), peerType: .channel), anchor: anchor)) + case let .instantView(webPage, anchor): + let browserController = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, webPage: webPage, anchor: anchor, sourceLocation: InstantPageSourceLocation(userLocation: .peer(strongSelf.peer.id), peerType: .channel)) + strongSelf.pushController(browserController) case let .join(link): strongSelf.presentController(JoinLinkPreviewController(context: strongSelf.context, link: link, navigateToPeer: { peer, peekData in if let strongSelf = self { @@ -1257,6 +1260,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case .premiumOffer: break + case .starsTopup: + break case let .joinVoiceChat(peerId, invite): strongSelf.presentController(VoiceChatJoinScreen(context: strongSelf.context, peerId: peerId, invite: invite, join: { call in }), .window(.root), nil) diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index 5cd1237b3fa..e07264e9c3e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -418,7 +418,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in }, tapMessage: { _ in - }, clickThroughMessage: { + }, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index d6556c800c0..66cb841f9c2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -137,15 +137,22 @@ private final class BalanceComponent: CombinedComponent { } private final class BadgeComponent: Component { + enum Direction { + case left + case right + } let theme: PresentationTheme let title: String + let inertiaDirection: Direction? init( theme: PresentationTheme, - title: String + title: String, + inertiaDirection: Direction? ) { self.theme = theme self.title = title + self.inertiaDirection = inertiaDirection } static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool { @@ -155,6 +162,9 @@ private final class BadgeComponent: Component { if lhs.title != rhs.title { return false } + if lhs.inertiaDirection != rhs.inertiaDirection { + return false + } return true } @@ -174,6 +184,7 @@ private final class BadgeComponent: Component { private var component: BadgeComponent? private var previousAvailableSize: CGSize? + private var previousInertiaDirection: BadgeComponent.Direction? override init(frame: CGRect) { self.badgeView = UIView() @@ -225,9 +236,8 @@ private final class BadgeComponent: Component { required init(coder: NSCoder) { preconditionFailure() } - + func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - if self.component == nil { self.badgeIcon.image = UIImage(bundleImageName: "Premium/SendStarsStarSliderIcon")?.withRenderingMode(.alwaysTemplate) } @@ -237,23 +247,8 @@ private final class BadgeComponent: Component { self.badgeLabel.color = .white - let countWidth: CGFloat - switch component.title.count { - case 1: - countWidth = 20.0 - case 2: - countWidth = 35.0 - case 3: - countWidth = 51.0 - case 4: - countWidth = 60.0 - case 5: - countWidth = 74.0 - case 6: - countWidth = 88.0 - default: - countWidth = 51.0 - } + let badgeLabelSize = self.badgeLabel.update(value: component.title, transition: .easeInOut(duration: 0.12)) + let countWidth: CGFloat = badgeLabelSize.width + 3.0 let badgeWidth: CGFloat = countWidth + 54.0 let badgeSize = CGSize(width: badgeWidth, height: 48.0) @@ -265,6 +260,25 @@ private final class BadgeComponent: Component { transition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0)) + if component.inertiaDirection != self.previousInertiaDirection { + self.previousInertiaDirection = component.inertiaDirection + + var angle: CGFloat = 0.0 + let transition: ContainedViewLayoutTransition + if let inertiaDirection = component.inertiaDirection { + switch inertiaDirection { + case .left: + angle = 0.22 + case .right: + angle = -0.22 + } + transition = .animated(duration: 0.45, curve: .spring) + } else { + transition = .animated(duration: 0.45, curve: .customSpring(damping: 65.0, initialVelocity: 0.0)) + } + transition.updateTransformRotation(view: self.badgeView, angle: angle) + } + self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeFullSize.width * 3.0, height: badgeFullSize.height)) if self.badgeForeground.animation(forKey: "movement") == nil { self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeFullSize.height / 2.0) @@ -276,8 +290,6 @@ private final class BadgeComponent: Component { self.badgeView.alpha = 1.0 let size = badgeSize - - let badgeLabelSize = self.badgeLabel.update(value: component.title, transition: .easeInOut(duration: 0.12)) transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: 14.0 + floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: 5.0), size: badgeLabelSize)) if self.previousAvailableSize != availableSize { @@ -651,9 +663,11 @@ private final class ChatSendStarsScreenComponent: Component { private let title = ComponentView() private let descriptionText = ComponentView() + private let badgeStars = BadgeStarsView() private let slider = ComponentView() private let sliderBackground = UIView() private let sliderForeground = UIView() + private let sliderStars = SliderStarsView() private let badge = ComponentView() private var topPeersLeftSeparator: SimpleLayer? @@ -703,9 +717,7 @@ private final class ChatSendStarsScreenComponent: Component { self.addSubview(self.dimView) self.layer.addSublayer(self.backgroundLayer) - - self.addSubview(self.navigationBarContainer) - + self.scrollView.delaysContentTouches = true self.scrollView.canCancelContentTouches = true self.scrollView.clipsToBounds = false @@ -728,6 +740,11 @@ 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(_:)))) } @@ -830,6 +847,10 @@ private final class ChatSendStarsScreenComponent: Component { } } + private var previousSliderValue: Float = 0.0 + private var previousTimestamp: Double? + private var inertiaDirection: BadgeComponent.Direction? + func update(component: ChatSendStarsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value let themeUpdated = self.environment?.theme !== environment.theme @@ -881,6 +902,53 @@ private final class ChatSendStarsScreenComponent: Component { } self.amount = 1 + Int64(value) self.state?.updated(transition: .immediate) + + let sliderValue = Float(value) / 1000.0 + let currentTimestamp = CACurrentMediaTime() + + if let previousTimestamp { + let deltaTime = currentTimestamp - previousTimestamp + let delta = sliderValue - self.previousSliderValue + let deltaValue = abs(sliderValue - self.previousSliderValue) + + let speed = deltaValue / Float(deltaTime) + let newSpeed = max(0, min(65.0, speed * 70.0)) + + var inertiaDirection: BadgeComponent.Direction? + if newSpeed >= 1.0 { + if delta > 0.0 { + inertiaDirection = .right + } else { + inertiaDirection = .left + } + } + if inertiaDirection != self.inertiaDirection { + self.inertiaDirection = inertiaDirection + self.state?.updated(transition: .immediate) + } + + if newSpeed < 0.01 && deltaValue < 0.001 { + + } else { + self.badgeStars.update(speed: newSpeed, delta: delta) + } + } + + self.previousSliderValue = sliderValue + self.previousTimestamp = currentTimestamp + }, + isTrackingUpdated: { [weak self] isTracking in + guard let self else { + return + } + if !isTracking { + self.previousTimestamp = nil + self.badgeStars.update(speed: 0.0) + } + if self.inertiaDirection != nil { + self.inertiaDirection = nil + self.state?.updated(transition: .immediate) + } } )), environment: {}, @@ -889,6 +957,7 @@ private final class ChatSendStarsScreenComponent: Component { let sliderFrame = CGRect(origin: CGPoint(x: sliderInset, y: contentHeight + 127.0), size: sliderSize) if let sliderView = self.slider.view { if sliderView.superview == nil { + self.scrollContentView.addSubview(self.badgeStars) self.scrollContentView.addSubview(self.sliderBackground) self.scrollContentView.addSubview(self.sliderForeground) self.scrollContentView.addSubview(sliderView) @@ -910,20 +979,30 @@ private final class ChatSendStarsScreenComponent: Component { 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 + var effectiveInertiaDirection = self.inertiaDirection + if progressFraction <= 0.03 || progressFraction >= 0.97 { + effectiveInertiaDirection = nil + } + let badgeSize = self.badge.update( transition: transition, component: AnyComponent(BadgeComponent( - theme: environment.theme, title: "\(self.amount)") - ), + theme: environment.theme, + title: "\(self.amount)", + inertiaDirection: effectiveInertiaDirection + )), environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) ) 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 { - self.scrollContentView.addSubview(badgeView) + self.scrollContentView.insertSubview(badgeView, belowSubview: self.badgeStars) } let badgeSideInset = sideInset + 15.0 @@ -943,6 +1022,10 @@ private final class ChatSendStarsScreenComponent: Component { badgeView.adjustTail(size: badgeSize, overflowWidth: -badgeOverflowWidth) } + + let starsRect = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: sliderForegroundFrame.midY)) + self.badgeStars.frame = starsRect + self.badgeStars.update(size: starsRect.size, emitterPosition: CGPoint(x: badgeFrame.minX, y: badgeFrame.midY - 64.0)) } contentHeight += 123.0 @@ -1437,3 +1520,198 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: context.strokePath() }) } + +private final class BadgeStarsView: UIView { + private let staticEmitterLayer = CAEmitterLayer() + private let dynamicEmitterLayer = CAEmitterLayer() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.staticEmitterLayer) + self.layer.addSublayer(self.dynamicEmitterLayer) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + private func setupEmitter() { + let color = UIColor(rgb: 0xffbe27) + + self.staticEmitterLayer.emitterShape = .circle + self.staticEmitterLayer.emitterSize = CGSize(width: 10.0, height: 5.0) + self.staticEmitterLayer.emitterMode = .outline + self.layer.addSublayer(self.staticEmitterLayer) + + self.dynamicEmitterLayer.birthRate = 0.0 + self.dynamicEmitterLayer.emitterShape = .circle + self.dynamicEmitterLayer.emitterSize = CGSize(width: 10.0, height: 55.0) + self.dynamicEmitterLayer.emitterMode = .surface + self.layer.addSublayer(self.dynamicEmitterLayer) + + let staticEmitter = CAEmitterCell() + staticEmitter.name = "emitter" + staticEmitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage + staticEmitter.birthRate = 20.0 + staticEmitter.lifetime = 2.7 + staticEmitter.velocity = 30.0 + staticEmitter.velocityRange = 3 + staticEmitter.scale = 0.15 + staticEmitter.scaleRange = 0.08 + staticEmitter.emissionRange = .pi * 2.0 + staticEmitter.setValue(3.0, forKey: "mass") + staticEmitter.setValue(2.0, forKey: "massRange") + + let dynamicEmitter = CAEmitterCell() + dynamicEmitter.name = "emitter" + dynamicEmitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage + dynamicEmitter.birthRate = 0.0 + dynamicEmitter.lifetime = 2.7 + dynamicEmitter.velocity = 30.0 + dynamicEmitter.velocityRange = 3 + dynamicEmitter.scale = 0.15 + dynamicEmitter.scaleRange = 0.08 + dynamicEmitter.emissionRange = .pi / 3.0 + dynamicEmitter.setValue(3.0, forKey: "mass") + dynamicEmitter.setValue(2.0, forKey: "massRange") + + let staticColors: [Any] = [ + UIColor.white.withAlphaComponent(0.0).cgColor, + UIColor.white.withAlphaComponent(0.35).cgColor, + color.cgColor, + color.cgColor, + color.withAlphaComponent(0.0).cgColor + ] + let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife") + staticColorBehavior.setValue(staticColors, forKey: "colors") + staticEmitter.setValue([staticColorBehavior], forKey: "emitterBehaviors") + + let dynamicColors: [Any] = [ + UIColor.white.withAlphaComponent(0.35).cgColor, + color.withAlphaComponent(0.85).cgColor, + color.cgColor, + color.cgColor, + color.withAlphaComponent(0.0).cgColor + ] + let dynamicColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife") + dynamicColorBehavior.setValue(dynamicColors, forKey: "colors") + dynamicEmitter.setValue([dynamicColorBehavior], forKey: "emitterBehaviors") + + let attractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor") + attractor.setValue("attractor", forKey: "name") + attractor.setValue(20, forKey: "falloff") + attractor.setValue(35, forKey: "radius") + self.staticEmitterLayer.setValue([attractor], forKey: "emitterBehaviors") + self.staticEmitterLayer.setValue(4.0, forKeyPath: "emitterBehaviors.attractor.stiffness") + self.staticEmitterLayer.setValue(false, forKeyPath: "emitterBehaviors.attractor.enabled") + + self.staticEmitterLayer.emitterCells = [staticEmitter] + self.dynamicEmitterLayer.emitterCells = [dynamicEmitter] + } + + func update(speed: Float, delta: Float? = nil) { + if speed > 0.0 { + if self.dynamicEmitterLayer.birthRate.isZero { + self.dynamicEmitterLayer.beginTime = CACurrentMediaTime() + } + + self.dynamicEmitterLayer.setValue(Float(20.0 + speed * 1.4), forKeyPath: "emitterCells.emitter.birthRate") + self.dynamicEmitterLayer.setValue(2.7 - min(1.1, 1.5 * speed / 120.0), forKeyPath: "emitterCells.emitter.lifetime") + self.dynamicEmitterLayer.setValue(30.0 + CGFloat(speed / 80.0), forKeyPath: "emitterCells.emitter.velocity") + + if let delta, speed > 15.0 { + self.dynamicEmitterLayer.setValue(delta > 0 ? .pi : 0, forKeyPath: "emitterCells.emitter.emissionLongitude") + self.dynamicEmitterLayer.setValue(.pi / 2.0, forKeyPath: "emitterCells.emitter.emissionRange") + } else { + self.dynamicEmitterLayer.setValue(0.0, forKeyPath: "emitterCells.emitter.emissionLongitude") + self.dynamicEmitterLayer.setValue(.pi * 2.0, forKeyPath: "emitterCells.emitter.emissionRange") + } + self.staticEmitterLayer.setValue(true, forKeyPath: "emitterBehaviors.attractor.enabled") + + self.dynamicEmitterLayer.birthRate = 1.0 + self.staticEmitterLayer.birthRate = 0.0 + } else { + self.dynamicEmitterLayer.birthRate = 0.0 + + if let staticEmitter = self.staticEmitterLayer.emitterCells?.first { + staticEmitter.beginTime = CACurrentMediaTime() + } + self.staticEmitterLayer.birthRate = 1.0 + self.staticEmitterLayer.setValue(false, forKeyPath: "emitterBehaviors.attractor.enabled") + } + } + + func update(size: CGSize, emitterPosition: CGPoint) { + if self.staticEmitterLayer.emitterCells == nil { + self.setupEmitter() + } + + self.staticEmitterLayer.frame = CGRect(origin: .zero, size: size) + self.staticEmitterLayer.emitterPosition = emitterPosition + + self.dynamicEmitterLayer.frame = CGRect(origin: .zero, size: size) + self.dynamicEmitterLayer.emitterPosition = emitterPosition + self.staticEmitterLayer.setValue(emitterPosition, forKeyPath: "emitterBehaviors.attractor.position") + } +} + +private final class SliderStarsView: UIView { + private let emitterLayer = CAEmitterLayer() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.emitterLayer) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + private func setupEmitter() { + self.emitterLayer.emitterShape = .rectangle + self.emitterLayer.emitterMode = .surface + self.layer.addSublayer(self.emitterLayer) + + let emitter = CAEmitterCell() + emitter.name = "emitter" + emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage + emitter.birthRate = 20.0 + emitter.lifetime = 2.0 + emitter.velocity = 15.0 + emitter.velocityRange = 10 + emitter.scale = 0.15 + emitter.scaleRange = 0.08 + emitter.emissionRange = .pi / 4.0 + emitter.setValue(3.0, forKey: "mass") + emitter.setValue(2.0, forKey: "massRange") + self.emitterLayer.emitterCells = [emitter] + + let colors: [Any] = [ + UIColor.white.withAlphaComponent(0.0).cgColor, + UIColor.white.withAlphaComponent(0.38).cgColor, + UIColor.white.withAlphaComponent(0.38).cgColor, + UIColor.white.withAlphaComponent(0.0).cgColor, + UIColor.white.withAlphaComponent(0.38).cgColor, + UIColor.white.withAlphaComponent(0.38).cgColor, + UIColor.white.withAlphaComponent(0.0).cgColor + ] + let colorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife") + colorBehavior.setValue(colors, forKey: "colors") + emitter.setValue([colorBehavior], forKey: "emitterBehaviors") + } + + func update(size: CGSize, value: CGFloat) { + if self.emitterLayer.emitterCells == nil { + self.setupEmitter() + } + + self.emitterLayer.setValue(20.0 + Float(value * 40.0), forKeyPath: "emitterCells.emitter.birthRate") + self.emitterLayer.setValue(15.0 + value * 75.0, forKeyPath: "emitterCells.emitter.velocity") + + self.emitterLayer.frame = CGRect(origin: .zero, size: size) + self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + self.emitterLayer.emitterSize = size + } +} diff --git a/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift index 5e54ec32151..d1210c4e147 100644 --- a/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift @@ -370,6 +370,7 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode { super.didLoad() self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + self.view.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:)))) } override public func animateIn() { @@ -491,9 +492,9 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode { } } - /*@objc func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.interfaceInteraction?.navigateToMessage(self.messageId, false, true, .generic) + @objc func longPressGesture(_ recognizer: UILongPressGestureRecognizer) { + if case .began = recognizer.state { + self.interfaceInteraction?.navigateToMessage(self.messageId, false, true, ChatLoadingMessageSubject.generic) } - }*/ + } } diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index a6e547d38fc..e0331cb2db3 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -98,12 +98,14 @@ public struct NavigateToMessageParams { public var quote: Quote? public var progress: Promise? public var forceNew: Bool + public var setupReply: Bool - public init(timestamp: Double?, quote: Quote?, progress: Promise? = nil, forceNew: Bool = false) { + public init(timestamp: Double?, quote: Quote?, progress: Promise? = nil, forceNew: Bool = false, setupReply: Bool = false) { self.timestamp = timestamp self.quote = quote self.progress = progress self.forceNew = forceNew + self.setupReply = setupReply } } @@ -182,7 +184,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let navigateToMessageStandalone: (MessageId) -> Void public let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void public let tapMessage: ((Message) -> Void)? - public let clickThroughMessage: () -> Void + public let clickThroughMessage: (UIView?, CGPoint?) -> Void public let toggleMessagesSelection: ([MessageId], Bool) -> Void public let sendCurrentMessage: (Bool, ChatSendMessageEffect?) -> Void public let sendMessage: (String) -> Void @@ -314,7 +316,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol navigateToMessageStandalone: @escaping (MessageId) -> Void, navigateToThreadMessage: @escaping (PeerId, Int64, MessageId?) -> Void, tapMessage: ((Message) -> Void)?, - clickThroughMessage: @escaping () -> Void, + clickThroughMessage: @escaping (UIView?, CGPoint?) -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool, ChatSendMessageEffect?) -> Void, sendMessage: @escaping (String) -> Void, diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index d8474a20731..cfd2961cb85 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -555,7 +555,12 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { let string = NSAttributedString(string: statusText, font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } else if let _ = user.botInfo { - let statusText = self.strings.Bot_GenericBotStatus + let statusText: String + if let subscriberCount = user.subscriberCount { + statusText = self.strings.Conversation_StatusBotSubscribers(subscriberCount) + } else { + statusText = self.strings.Bot_GenericBotStatus + } let string = NSAttributedString(string: statusText, font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 803ff111242..6442a231f7d 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -438,6 +438,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { if tinted { self.updateTintColor() } + case .ton: + self.updateTon() } } else if let file = file { self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad) @@ -623,6 +625,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.contents = tinted ? tintedStarImage?.cgImage : starImage?.cgImage } + private func updateTon() { + self.contents = tonImage?.cgImage + } + private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) { guard let arguments = self.arguments else { return @@ -770,7 +776,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } public final class EmojiTextAttachmentView: UIView { - private let contentLayer: InlineStickerItemLayer + public let contentLayer: InlineStickerItemLayer public var isActive: Bool = true { didSet { @@ -820,7 +826,7 @@ public final class EmojiTextAttachmentView: UIView { public final class CustomEmojiContainerView: UIView { private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView? - private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:] + public private(set) var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:] public init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) { self.emojiViewProvider = emojiViewProvider @@ -899,7 +905,17 @@ 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: 2.0, dy: 2.0), byTiling: false) + context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 4.0), byTiling: false) + } + })?.withRenderingMode(.alwaysTemplate) +}() + +private let tonImage: UIImage? = { + generateImage(CGSize(width: 32.0, height: 32.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonBig"), color: UIColor(rgb: 0x007aff)), let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 4.0), byTiling: false) } })?.withRenderingMode(.alwaysTemplate) }() diff --git a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/BUILD b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/BUILD index 3c890d23856..739b84471e0 100644 --- a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/BUILD +++ b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/BUILD @@ -14,6 +14,7 @@ swift_library( "//submodules/ComponentFlow", "//submodules/Components/AnimatedStickerComponent", "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramPresentationData", "//submodules/AccountContext", diff --git a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift index a9777433301..38bc8689a42 100644 --- a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift @@ -7,30 +7,33 @@ import ButtonComponent import TelegramPresentationData import AccountContext import MultilineTextComponent +import BalancedTextComponent public final class EmptyStateIndicatorComponent: Component { public let context: AccountContext public let theme: PresentationTheme - public let animationName: String - public let title: String + public let animationName: String? + public let title: String? public let text: String public let actionTitle: String? public let fitToHeight: Bool public let action: () -> Void public let additionalActionTitle: String? public let additionalAction: () -> Void + public let additionalActionSeparator: String? public init( context: AccountContext, theme: PresentationTheme, fitToHeight: Bool, - animationName: String, - title: String, + animationName: String?, + title: String?, text: String, actionTitle: String?, action: @escaping () -> Void, additionalActionTitle: String?, - additionalAction: @escaping () -> Void + additionalAction: @escaping () -> Void, + additionalActionSeparator: String? = nil ) { self.context = context self.theme = theme @@ -42,6 +45,7 @@ public final class EmptyStateIndicatorComponent: Component { self.action = action self.additionalActionTitle = additionalActionTitle self.additionalAction = additionalAction + self.additionalActionSeparator = additionalActionSeparator } public static func ==(lhs: EmptyStateIndicatorComponent, rhs: EmptyStateIndicatorComponent) -> Bool { @@ -69,6 +73,9 @@ public final class EmptyStateIndicatorComponent: Component { if lhs.additionalActionTitle != rhs.additionalActionTitle { return false } + if lhs.additionalActionSeparator != rhs.additionalActionSeparator { + return false + } return true } @@ -81,6 +88,9 @@ public final class EmptyStateIndicatorComponent: Component { private let text = ComponentView() private var button: ComponentView? private var additionalButton: ComponentView? + private var additionalSeparatorLeft: SimpleLayer? + private var additionalSeparatorRight: SimpleLayer? + private var additionalSeparatorText: ComponentView? override public init(frame: CGRect) { super.init(frame: frame) @@ -94,35 +104,42 @@ public final class EmptyStateIndicatorComponent: Component { self.component = component self.componentState = state - let animationSize = self.animation.update( - transition: transition, - component: AnyComponent(AnimatedStickerComponent( - account: component.context.account, - animation: AnimatedStickerComponent.Animation(source: .bundle(name: component.animationName), loop: true), - size: CGSize(width: 120.0, height: 120.0) - )), - environment: {}, - containerSize: CGSize(width: 120.0, height: 120.0) - ) - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 0 - )), - environment: {}, - containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0) - ) + var animationSize: CGSize? + if let animationName = component.animationName { + animationSize = self.animation.update( + transition: transition, + component: AnyComponent(AnimatedStickerComponent( + account: component.context.account, + animation: AnimatedStickerComponent.Animation(source: .bundle(name: animationName), loop: true), + size: CGSize(width: 120.0, height: 120.0) + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 120.0) + ) + } + + var titleSize: CGSize? + if let title = component.title { + titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0) + ) + } let textSize = self.text.update( transition: .immediate, - component: AnyComponent(MultilineTextComponent( + component: AnyComponent(BalancedTextComponent( text: .plain(NSAttributedString(string: component.text, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0 )), environment: {}, - containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0) + containerSize: CGSize(width: min(400.0, availableSize.width - 16.0 * 2.0), height: 1000.0) ) var buttonSize: CGSize? if let actionTitle = component.actionTitle { @@ -199,14 +216,80 @@ public final class EmptyStateIndicatorComponent: Component { } } + var additionalSeparatorTextSize: CGSize? + if let additionalActionSeparator = component.additionalActionSeparator { + let additionalSeparatorText: ComponentView + if let current = self.additionalSeparatorText { + additionalSeparatorText = current + } else { + additionalSeparatorText = ComponentView() + self.additionalSeparatorText = additionalSeparatorText + } + + let additionalSeparatorLeft: SimpleLayer + if let current = self.additionalSeparatorLeft { + additionalSeparatorLeft = current + } else { + additionalSeparatorLeft = SimpleLayer() + self.additionalSeparatorLeft = additionalSeparatorLeft + self.layer.addSublayer(additionalSeparatorLeft) + } + + let additionalSeparatorRight: SimpleLayer + if let current = self.additionalSeparatorRight { + additionalSeparatorRight = current + } else { + additionalSeparatorRight = SimpleLayer() + self.additionalSeparatorRight = additionalSeparatorRight + self.layer.addSublayer(additionalSeparatorRight) + } + + additionalSeparatorLeft.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + additionalSeparatorRight.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + + additionalSeparatorTextSize = additionalSeparatorText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: additionalActionSeparator, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 100.0) + ) + } else { + if let additionalSeparatorLeft = self.additionalSeparatorLeft { + self.additionalSeparatorLeft = nil + additionalSeparatorLeft.removeFromSuperlayer() + } + if let additionalSeparatorRight = self.additionalSeparatorRight { + self.additionalSeparatorRight = nil + additionalSeparatorRight.removeFromSuperlayer() + } + if let additionalSeparatorText = self.additionalSeparatorText { + self.additionalSeparatorText = nil + additionalSeparatorText.view?.removeFromSuperview() + } + } + let animationSpacing: CGFloat = 11.0 let titleSpacing: CGFloat = 17.0 let buttonSpacing: CGFloat = 21.0 + let additionalSeparatorHeight: CGFloat = 31.0 + + var totalHeight: CGFloat = 0.0 - var totalHeight: CGFloat = animationSize.height + animationSpacing + titleSize.height + titleSpacing + textSize.height + if let animationSize { + totalHeight += animationSize.height + animationSpacing + } + if let titleSize { + totalHeight += titleSize.height + titleSpacing + } + totalHeight += textSize.height if let buttonSize { totalHeight += buttonSpacing + buttonSize.height } + if let _ = additionalSeparatorTextSize { + totalHeight += additionalSeparatorHeight + } if let additionalButtonSize { totalHeight += buttonSpacing + additionalButtonSize.height } @@ -218,14 +301,14 @@ public final class EmptyStateIndicatorComponent: Component { contentY = floor((availableSize.height - totalHeight) * 0.5) } - if let animationView = self.animation.view { + if let animationSize, let animationView = self.animation.view { if animationView.superview == nil { self.addSubview(animationView) } transition.setFrame(view: animationView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - animationSize.width) * 0.5), y: contentY), size: animationSize)) contentY += animationSize.height + animationSpacing } - if let titleView = self.title.view { + if let titleSize, let titleView = self.title.view { if titleView.superview == nil { self.addSubview(titleView) } @@ -246,6 +329,25 @@ public final class EmptyStateIndicatorComponent: Component { transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: contentY), size: buttonSize)) contentY += buttonSize.height + buttonSpacing } + + if let additionalSeparatorTextSize, let additionalSeparatorText = self.additionalSeparatorText, let additionalSeparatorLeft = self.additionalSeparatorLeft, let additionalSeparatorRight = self.additionalSeparatorRight { + let additionalSeparatorTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - additionalSeparatorTextSize.width) * 0.5), y: contentY), size: additionalSeparatorTextSize) + if let additionalSeparatorTextView = additionalSeparatorText.view { + if additionalSeparatorTextView.superview == nil { + self.addSubview(additionalSeparatorTextView) + } + transition.setFrame(view: additionalSeparatorTextView, frame: additionalSeparatorTextFrame) + } + + let separatorWidth: CGFloat = 72.0 + let separatorSpacing: CGFloat = 10.0 + + transition.setFrame(layer: additionalSeparatorLeft, frame: CGRect(origin: CGPoint(x: additionalSeparatorTextFrame.minX - separatorSpacing - separatorWidth, y: additionalSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel))) + transition.setFrame(layer: additionalSeparatorRight, frame: CGRect(origin: CGPoint(x: additionalSeparatorTextFrame.maxX + separatorSpacing, y: additionalSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel))) + + contentY += additionalSeparatorHeight + } + if let additionalButtonSize, let additionalButtonView = self.additionalButton?.view { if additionalButtonView.superview == nil { self.addSubview(additionalButtonView) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 69cecf479e9..bc898fe289c 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -710,7 +710,7 @@ public final class EntityKeyboardComponent: Component { deleteBackwards?() AudioServicesPlaySystemSound(1155) } - ).withHoldAction({ + ).withHoldAction({ _ in deleteBackwards?() AudioServicesPlaySystemSound(1155) }).minSize(CGSize(width: 38.0, height: 38.0))))) diff --git a/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift b/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift index 15c955e0b35..e133ecbacbf 100644 --- a/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift +++ b/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift @@ -126,7 +126,7 @@ public func paneGifSearchForQuery(context: AccountContext, query: String, offset )) } } - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil)]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)]) references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result))) } case let .internalReference(internalReference): diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index 677c0472156..2868bce6050 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -494,6 +494,9 @@ public final class InteractiveTextNodeLayout: NSObject { public var trailingLineWidth: CGFloat { if let lastSegment = self.segments.last, let lastLine = lastSegment.lines.last { var width = lastLine.frame.maxX + if let additionalTrailingLine = lastLine.additionalTrailingLine { + width += additionalTrailingLine.1 + } if let blockQuote = lastSegment.blockQuote { if lastLine.frame.intersects(blockQuote.frame) { @@ -1606,7 +1609,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn spoilerWords: [], embeddedItems: [], attachments: [], - additionalTrailingLine: (truncationToken, 0.0) + additionalTrailingLine: (truncationToken, truncationTokenWidth) ) } } diff --git a/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift index a056c15954d..b10e34b9d13 100644 --- a/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift +++ b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift @@ -235,7 +235,7 @@ public func legacyInstantVideoController(theme: PresentationTheme, forStory: Boo } } - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: finalDuration, size: PixelDimensions(finalDimensions), flags: [.instantRoundVideo], preloadSize: nil)]) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: finalDuration, size: PixelDimensions(finalDimensions), flags: [.instantRoundVideo], preloadSize: nil, coverTime: nil)]) var message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let scheduleTime: Int32? = scheduleTimestamp > 0 ? scheduleTimestamp : nil diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index ad6000d2ccd..811350c9c96 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -1,5 +1,6 @@ import Foundation import TelegramCore +import UrlEscaping public func decodeCodableDrawingEntities(data: Data) -> [CodableDrawingEntity] { if let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: data) { @@ -24,6 +25,7 @@ public enum CodableDrawingEntity: Equatable { case vector(DrawingVectorEntity) case location(DrawingLocationEntity) case link(DrawingLinkEntity) + case weather(DrawingWeatherEntity) public init?(entity: DrawingEntity) { if let entity = entity as? DrawingStickerEntity { @@ -40,6 +42,8 @@ public enum CodableDrawingEntity: Equatable { self = .location(entity) } else if let entity = entity as? DrawingLinkEntity { self = .link(entity) + } else if let entity = entity as? DrawingWeatherEntity { + self = .weather(entity) } else { return nil } @@ -61,6 +65,8 @@ public enum CodableDrawingEntity: Equatable { return entity case let .link(entity): return entity + case let .weather(entity): + return entity } } @@ -109,6 +115,14 @@ public enum CodableDrawingEntity: Equatable { size = entitySize } } + case let .weather(entity): + position = entity.position + size = entity.renderImage?.size + rotation = entity.rotation + scale = entity.scale + if let size { + cornerRadius = (size.height * 0.17) / size.width + } default: return nil } @@ -170,13 +184,27 @@ public enum CodableDrawingEntity: Equatable { return nil } case let .link(entity): - var url = entity.url - if !url.hasPrefix("http://") && !url.hasPrefix("https://") { - url = "https://\(url)" - } return .link( coordinates: coordinates, - url: url + url: explicitUrl(entity.url) + ) + case let .weather(entity): + let color: UInt32 + switch entity.style { + case .white: + color = 0xffffffff + case .black: + color = 0xff000000 + case .transparent: + color = 0x51000000 + case .custom: + color = entity.color.toUIColor().argb + } + return .weather( + coordinates: coordinates, + emoji: entity.emoji, + temperature: entity.temperature, + color: Int32(bitPattern: color) ) default: return nil @@ -198,6 +226,7 @@ extension CodableDrawingEntity: Codable { case vector case location case link + case weather } public init(from decoder: Decoder) throws { @@ -218,6 +247,8 @@ extension CodableDrawingEntity: Codable { self = .location(try container.decode(DrawingLocationEntity.self, forKey: .entity)) case .link: self = .link(try container.decode(DrawingLinkEntity.self, forKey: .entity)) + case .weather: + self = .weather(try container.decode(DrawingWeatherEntity.self, forKey: .entity)) } } @@ -245,6 +276,9 @@ extension CodableDrawingEntity: Codable { case let .link(payload): try container.encode(EntityType.link, forKey: .type) try container.encode(payload, forKey: .entity) + case let .weather(payload): + try container.encode(EntityType.weather, forKey: .type) + try container.encode(payload, forKey: .entity) } } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift index 02407f5e43a..b07bd48a154 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift @@ -88,11 +88,15 @@ public final class DrawingTextEntity: DrawingEntity, Codable { return true } var isAnimated = false - self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in - if let _ = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { - isAnimated = true + + if let renderSubEntities = self.renderSubEntities { + for entity in renderSubEntities { + if entity.isAnimated { + isAnimated = true + break + } } - }) + } return isAnimated } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingWeatherEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingWeatherEntity.swift new file mode 100644 index 00000000000..ed4f63ef73a --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingWeatherEntity.swift @@ -0,0 +1,193 @@ +import Foundation +import UIKit +import Display +import AccountContext +import TextFormat +import Postbox +import TelegramCore + +public final class DrawingWeatherEntity: DrawingEntity, Codable { + private enum CodingKeys: String, CodingKey { + case uuid + case style + case color + case hasCustomColor + case emoji + case temperature + case icon + case referenceDrawingSize + case position + case width + case scale + case rotation + case renderImage + } + + public enum Style: Codable, Equatable { + case white + case black + case transparent + case custom + } + + public var uuid: UUID + public var isAnimated: Bool { + return false + } + + + public var style: Style + public var icon: TelegramMediaFile? + public var emoji: String + public var temperature: Double + + public var color: DrawingColor = DrawingColor(color: .white) { + didSet { + if self.color.toUIColor().argb == UIColor.white.argb { + self.style = .white + self.hasCustomColor = false + } else { + self.style = .custom + self.hasCustomColor = true + } + } + } + public var hasCustomColor = false + public var lineWidth: CGFloat = 0.0 + + public var referenceDrawingSize: CGSize + public var position: CGPoint + public var width: CGFloat + public var scale: CGFloat { + didSet { + self.scale = min(2.5, self.scale) + } + } + public var rotation: CGFloat + + public var center: CGPoint { + return self.position + } + + public var renderImage: UIImage? + public var renderSubEntities: [DrawingEntity]? + + public var isMedia: Bool { + return false + } + + public init(emoji: String, emojiFile: TelegramMediaFile?, temperature: Double, style: Style) { + self.uuid = UUID() + + self.emoji = emoji + self.icon = emojiFile + self.temperature = temperature + self.style = style + + self.referenceDrawingSize = .zero + self.position = .zero + self.width = 100.0 + self.scale = 1.0 + self.rotation = 0.0 + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.uuid = try container.decode(UUID.self, forKey: .uuid) + self.emoji = try container.decode(String.self, forKey: .emoji) + self.temperature = try container.decode(Double.self, forKey: .temperature) + self.style = try container.decode(Style.self, forKey: .style) + self.color = try container.decodeIfPresent(DrawingColor.self, forKey: .color) ?? DrawingColor(color: .white) + self.hasCustomColor = try container.decodeIfPresent(Bool.self, forKey: .hasCustomColor) ?? false + + if let iconData = try container.decodeIfPresent(Data.self, forKey: .icon) { + self.icon = PostboxDecoder(buffer: MemoryBuffer(data: iconData)).decodeRootObject() as? TelegramMediaFile + } + + self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) + self.position = try container.decode(CGPoint.self, forKey: .position) + self.width = try container.decode(CGFloat.self, forKey: .width) + self.scale = try container.decode(CGFloat.self, forKey: .scale) + self.rotation = try container.decode(CGFloat.self, forKey: .rotation) + if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { + self.renderImage = UIImage(data: renderImageData) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.uuid, forKey: .uuid) + try container.encode(self.emoji, forKey: .emoji) + try container.encode(self.temperature, forKey: .temperature) + try container.encode(self.style, forKey: .style) + try container.encode(self.color, forKey: .color) + try container.encode(self.hasCustomColor, forKey: .hasCustomColor) + + var encoder = PostboxEncoder() + if let icon = self.icon { + encoder = PostboxEncoder() + encoder.encodeRootObject(icon) + let iconData = encoder.makeData() + try container.encode(iconData, forKey: .icon) + } + + try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) + try container.encode(self.position, forKey: .position) + try container.encode(self.width, forKey: .width) + try container.encode(self.scale, forKey: .scale) + try container.encode(self.rotation, forKey: .rotation) + if let renderImage, let data = renderImage.pngData() { + try container.encode(data, forKey: .renderImage) + } + } + + public func duplicate(copy: Bool) -> DrawingEntity { + let newEntity = DrawingWeatherEntity(emoji: self.emoji, emojiFile: self.icon, temperature: self.temperature, style: self.style) + if copy { + newEntity.uuid = self.uuid + } + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.width = self.width + newEntity.scale = self.scale + newEntity.rotation = self.rotation + return newEntity + } + + public func isEqual(to other: DrawingEntity) -> Bool { + guard let other = other as? DrawingWeatherEntity else { + return false + } + if self.uuid != other.uuid { + return false + } + if self.emoji != other.emoji { + return false + } + if self.temperature != other.temperature { + return false + } + if self.style != other.style { + return false + } + if self.color != other.color { + return false + } + if self.referenceDrawingSize != other.referenceDrawingSize { + return false + } + if self.position != other.position { + return false + } + if self.width != other.width { + return false + } + if self.scale != other.scale { + return false + } + if self.rotation != other.rotation { + return false + } + return true + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 8cbf706e034..09b076bbd25 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -476,6 +476,7 @@ public final class MediaEditor { audioTrackOffset: nil, audioTrackVolume: nil, audioTrackSamples: nil, + coverImageTimestamp: nil, qualityPreset: nil ) } @@ -498,6 +499,9 @@ public final class MediaEditor { } else if case let .video(_, _, _, _, _, duration) = subject { self.playerPlaybackState = PlaybackState(duration: duration, position: 0.0, isPlaying: false, hasAudio: true) self.playerPlaybackStatePromise.set(.single(self.playerPlaybackState)) + } else if case let .draft(mediaEditorDraft) = subject, mediaEditorDraft.isVideo { + self.playerPlaybackState = PlaybackState(duration: mediaEditorDraft.duration ?? 0.0, position: 0.0, isPlaying: false, hasAudio: true) + self.playerPlaybackStatePromise.set(.single(self.playerPlaybackState)) } } @@ -519,7 +523,7 @@ public final class MediaEditor { self.renderer.consume(main: .texture(texture, time, hasTransparency), additional: additionalTexture.flatMap { .texture($0, time, false) }, render: true, displayEnabled: false) } - private func setupSource() { + private func setupSource(andPlay: Bool) { guard let renderTarget = self.previewView else { return } @@ -829,6 +833,9 @@ public final class MediaEditor { self.setupTimeObservers() Queue.mainQueue().justDispatch { let startPlayback = { + guard andPlay else { + return + } player.playImmediately(atRate: 1.0) // additionalPlayer?.playImmediately(atRate: 1.0) self.audioPlayer?.playImmediately(atRate: 1.0) @@ -940,13 +947,13 @@ public final class MediaEditor { self.audioDelayTimer = nil } - public func attachPreviewView(_ previewView: MediaEditorPreviewView) { + public func attachPreviewView(_ previewView: MediaEditorPreviewView, andPlay: Bool) { self.previewView?.renderer = nil self.previewView = previewView previewView.renderer = self.renderer - self.setupSource() + self.setupSource(andPlay: andPlay) } private var skipRendering = false @@ -1507,7 +1514,15 @@ public final class MediaEditor { public func setVideoTrimRange(_ trimRange: Range, apply: Bool) { self.updateValues(mode: .skipRendering) { values in - return values.withUpdatedVideoTrimRange(trimRange) + var updatedValues = values.withUpdatedVideoTrimRange(trimRange) + if let coverImageTimestamp = updatedValues.coverImageTimestamp { + if coverImageTimestamp < trimRange.lowerBound { + updatedValues = updatedValues.withUpdatedCoverImageTimestamp(trimRange.lowerBound) + } else if coverImageTimestamp > trimRange.upperBound { + updatedValues = updatedValues.withUpdatedCoverImageTimestamp(trimRange.upperBound) + } + } + return updatedValues } if apply { @@ -1733,6 +1748,12 @@ public final class MediaEditor { } } + public func setCoverImageTimestamp(_ coverImageTimestamp: Double?) { + self.updateValues(mode: .skipRendering) { values in + return values.withUpdatedCoverImageTimestamp(coverImageTimestamp) + } + } + public func setDrawingAndEntities(data: Data?, image: UIImage?, entities: [CodableDrawingEntity]) { self.updateValues(mode: .skipRendering) { values in return values.withUpdatedDrawingAndEntities(drawing: image, entities: entities) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift index b38a45fcad6..4a8440725b9 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -28,6 +28,10 @@ private func prerenderTextTransformations(entity: DrawingEntity, image: UIImage, angle = -entity.rotation scale = entity.scale position = entity.position + } else if let entity = entity as? DrawingWeatherEntity { + angle = -entity.rotation + scale = entity.scale + position = entity.position } else if let entity = entity as? DrawingLinkEntity { angle = -entity.rotation scale = entity.scale @@ -66,7 +70,9 @@ private func prerenderTextTransformations(entity: DrawingEntity, image: UIImage, } func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, entity: DrawingEntity, colorSpace: CGColorSpace, tintColor: UIColor? = nil) -> [MediaEditorComposerEntity] { - if let entity = entity as? DrawingStickerEntity { + if entity is DrawingWeatherEntity { + return [] + } else if let entity = entity as? DrawingStickerEntity { if case let .file(_, type) = entity.content, case .reaction = type { return [] } else { @@ -124,6 +130,8 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)] } else if let entity = entity as? DrawingLinkEntity { return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)] + } else if let entity = entity as? DrawingWeatherEntity { + return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)] } } return [] diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index deec4efc3d0..828e7500d8f 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -324,6 +324,9 @@ public final class MediaEditorValues: Codable, Equatable { if lhs.audioTrackSamples != rhs.audioTrackSamples { return false } + if lhs.coverImageTimestamp != rhs.coverImageTimestamp { + return false + } if lhs.nightTheme != rhs.nightTheme { return false } @@ -394,6 +397,7 @@ public final class MediaEditorValues: Codable, Equatable { case audioTrackTrimRange case audioTrackOffset case audioTrackVolume + case coverImageTimestamp case qualityPreset } @@ -438,6 +442,8 @@ public final class MediaEditorValues: Codable, Equatable { public let audioTrackVolume: CGFloat? public let audioTrackSamples: MediaAudioTrackSamples? + public let coverImageTimestamp: Double? + public let qualityPreset: MediaQualityPreset? var isStory: Bool { @@ -486,6 +492,7 @@ public final class MediaEditorValues: Codable, Equatable { audioTrackOffset: Double?, audioTrackVolume: CGFloat?, audioTrackSamples: MediaAudioTrackSamples?, + coverImageTimestamp: Double?, qualityPreset: MediaQualityPreset? ) { self.peerId = peerId @@ -521,6 +528,7 @@ public final class MediaEditorValues: Codable, Equatable { self.audioTrackOffset = audioTrackOffset self.audioTrackVolume = audioTrackVolume self.audioTrackSamples = audioTrackSamples + self.coverImageTimestamp = coverImageTimestamp self.qualityPreset = qualityPreset } @@ -591,6 +599,8 @@ public final class MediaEditorValues: Codable, Equatable { self.audioTrackSamples = nil + self.coverImageTimestamp = try container.decodeIfPresent(Double.self, forKey: .coverImageTimestamp) + self.qualityPreset = (try container.decodeIfPresent(Int32.self, forKey: .qualityPreset)).flatMap { MediaQualityPreset(rawValue: $0) } } @@ -652,109 +662,115 @@ public final class MediaEditorValues: Codable, Equatable { try container.encodeIfPresent(self.audioTrackOffset, forKey: .audioTrackOffset) try container.encodeIfPresent(self.audioTrackVolume, forKey: .audioTrackVolume) + try container.encodeIfPresent(self.coverImageTimestamp, forKey: .coverImageTimestamp) + try container.encodeIfPresent(self.qualityPreset?.rawValue, forKey: .qualityPreset) } public func makeCopy() -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedCrop(offset: CGPoint, scale: CGFloat, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: offset, cropRect: self.cropRect, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: offset, cropRect: self.cropRect, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } public func withUpdatedCropRect(cropRect: CGRect, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: .zero, cropRect: cropRect, cropScale: 1.0, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: .zero, cropRect: cropRect, cropScale: 1.0, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedGradientColors(gradientColors: [UIColor]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedVideoIsMuted(_ videoIsMuted: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedVideoIsFullHd(_ videoIsFullHd: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedVideoIsMirrored(_ videoIsMirrored: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedVideoVolume(_ videoVolume: CGFloat?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedAdditionalVideo(path: String?, isDual: Bool, positionChanges: [VideoPositionChange]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: path, additionalVideoIsDual: isDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: positionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: path, additionalVideoIsDual: isDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: positionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedAdditionalVideo(position: CGPoint, scale: CGFloat, rotation: CGFloat) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: position, additionalVideoScale: scale, additionalVideoRotation: rotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: position, additionalVideoScale: scale, additionalVideoRotation: rotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedAdditionalVideoTrimRange(_ additionalVideoTrimRange: Range?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedAdditionalVideoOffset(_ additionalVideoOffset: Double?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedAdditionalVideoVolume(_ additionalVideoVolume: CGFloat?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedVideoTrimRange(_ videoTrimRange: Range) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedDrawingAndEntities(drawing: UIImage?, entities: [CodableDrawingEntity]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: drawing, maskDrawing: self.maskDrawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: drawing, maskDrawing: self.maskDrawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } public func withUpdatedMaskDrawing(maskDrawing: UIImage?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedToolValues(_ toolValues: [EditorToolKey: Any]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedAudioTrack(_ audioTrack: MediaAudioTrack?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedAudioTrackTrimRange(_ audioTrackTrimRange: Range?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedAudioTrackOffset(_ audioTrackOffset: Double?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedAudioTrackVolume(_ audioTrackVolume: CGFloat?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedAudioTrackSamples(_ audioTrackSamples: MediaAudioTrackSamples?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } func withUpdatedNightTheme(_ nightTheme: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) } public func withUpdatedEntities(_ entities: [CodableDrawingEntity]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: self.qualityPreset) + } + + public func withUpdatedCoverImageTimestamp(_ coverImageTimestamp: Double?) -> MediaEditorValues { + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: coverImageTimestamp, qualityPreset: self.qualityPreset) } public func withUpdatedQualityPreset(_ qualityPreset: MediaQualityPreset?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, coverImageTimestamp: self.coverImageTimestamp, qualityPreset: qualityPreset) } public var resultDimensions: PixelDimensions { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index fcbde92d5dd..72d3150645f 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -63,6 +63,8 @@ swift_library( "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/WebsiteType", "//submodules/UrlEscaping", + "//submodules/DeviceLocationManager", + "//submodules/TelegramUI/Components/SaveProgressScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift index b8afdb50818..8b64da17aee 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift @@ -466,14 +466,21 @@ private final class CreateLinkSheetComponent: CombinedComponent { let text = !self.name.isEmpty ? self.name : self.link var effectiveMedia: TelegramMediaWebpage? - if let webpage = self.webpage, case .Loaded = webpage.content, !self.dismissed { + var webpageHasLargeMedia = false + if let webpage = self.webpage, case let .Loaded(content) = webpage.content, !self.dismissed { effectiveMedia = webpage + + if let isMediaLargeByDefault = content.isMediaLargeByDefault, isMediaLargeByDefault { + webpageHasLargeMedia = true + } else { + webpageHasLargeMedia = true + } } var attributes: [MessageAttribute] = [] attributes.append(TextEntitiesMessageAttribute(entities: [.init(range: 0 ..< (text as NSString).length, type: .Url)])) if !self.dismissed { - attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !self.positionBelowText, forceLargeMedia: self.largeMedia, isManuallyAdded: false, isSafe: true)) + attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !self.positionBelowText, forceLargeMedia: self.largeMedia ?? webpageHasLargeMedia, isManuallyAdded: false, isSafe: true)) } let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift index c45a76b7c1d..9b259b362cb 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift @@ -16,6 +16,7 @@ public extension MediaEditorScreen { peer: EnginePeer, storyItem: EngineStoryItem, videoPlaybackPosition: Double?, + cover: Bool, repost: Bool, transitionIn: MediaEditorScreen.TransitionIn, transitionOut: MediaEditorScreen.TransitionOut?, @@ -82,12 +83,24 @@ public extension MediaEditorScreen { transitionOut: nil ) + var videoPlaybackPosition = videoPlaybackPosition + if cover, case let .file(file) = storyItem.media { + videoPlaybackPosition = 0.0 + for attribute in file.attributes { + if case let .Video(_, _, _, _, coverTime) = attribute { + videoPlaybackPosition = coverTime + break + } + } + } + var updateProgressImpl: ((Float) -> Void)? let controller = MediaEditorScreen( context: context, mode: .storyEditor, subject: subject, isEditing: !repost, + isEditingCover: cover, forwardSource: repost ? (peer, storyItem) : nil, initialCaption: initialCaption, initialPrivacy: initialPrivacy, @@ -152,11 +165,15 @@ public extension MediaEditorScreen { }) } else { var updatedText: String? + var updatedCoverTimestamp: Double? var updatedEntities: [MessageTextEntity]? if result.caption.string != storyItem.text || entities != storyItem.entities { updatedText = result.caption.string updatedEntities = entities } + if let coverTimestamp = result.coverTimestamp { + updatedCoverTimestamp = coverTimestamp + } if let mediaResult = result.media { switch mediaResult { @@ -216,7 +233,7 @@ public extension MediaEditorScreen { } } - update((context.engine.messages.editStory(peerId: peer.id, id: storyItem.id, media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers), mediaAreas: result.mediaAreas, text: updatedText, entities: updatedEntities, privacy: nil) + update((context.engine.messages.editStory(peerId: peer.id, id: storyItem.id, media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers, coverTime: nil), mediaAreas: result.mediaAreas, text: updatedText, entities: updatedEntities, privacy: nil) |> deliverOnMainQueue).startStrict(next: { result in switch result { case let .progress(progress): @@ -235,8 +252,22 @@ public extension MediaEditorScreen { default: break } - } else if updatedText != nil { - let _ = (context.engine.messages.editStory(peerId: peer.id, id: storyItem.id, media: nil, mediaAreas: nil, text: updatedText, entities: updatedEntities, privacy: nil) + } else if updatedText != nil || updatedCoverTimestamp != nil { + var media: EngineStoryInputMedia? + if let updatedCoverTimestamp { + if case let .file(file) = storyItem.media { + var updatedAttributes: [TelegramMediaFileAttribute] = [] + for attribute in file.attributes { + if case let .Video(duration, size, flags, preloadSize, _) = attribute { + updatedAttributes.append(.Video(duration: duration, size: size, flags: flags, preloadSize: preloadSize, coverTime: min(duration, updatedCoverTimestamp))) + } else { + updatedAttributes.append(attribute) + } + } + media = .existing(media: file.withUpdatedAttributes(updatedAttributes)) + } + } + let _ = (context.engine.messages.editStory(peerId: peer.id, id: storyItem.id, media: media, mediaAreas: nil, text: updatedText, entities: updatedEntities, privacy: nil) |> deliverOnMainQueue).startStandalone(next: { result in switch result { case .completed: diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift new file mode 100644 index 00000000000..db9dc21545c --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift @@ -0,0 +1,609 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import MediaEditor +import MediaScrubberComponent +import ButtonComponent + +private final class MediaCoverScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let mediaEditor: Signal + let exclusive: Bool + + init( + context: AccountContext, + mediaEditor: Signal, + exclusive: Bool + ) { + self.context = context + self.mediaEditor = mediaEditor + self.exclusive = exclusive + } + + static func ==(lhs: MediaCoverScreenComponent, rhs: MediaCoverScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.exclusive != rhs.exclusive { + return false + } + return true + } + + final class State: ComponentState { + var playerStateDisposable: Disposable? + var playerState: MediaEditorPlayerState? + + private(set) var mediaEditor: MediaEditor? + + init(mediaEditor: Signal) { + super.init() + + let _ = (mediaEditor + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] mediaEditor in + if let self, let mediaEditor { + self.mediaEditor = mediaEditor + + self.playerStateDisposable = (mediaEditor.playerState(framesCount: 16) + |> deliverOnMainQueue).start(next: { [weak self] playerState in + if let self { + if self.playerState != playerState { + self.playerState = playerState + self.updated() + } + } + }) + } + }) + } + + deinit { + self.playerStateDisposable?.dispose() + } + } + + func makeState() -> State { + return State(mediaEditor: self.mediaEditor) + } + + public final class View: UIView { + private let buttonsContainerView = UIView() + private let buttonsBackgroundView = UIImageView() + private let previewContainerView = UIView() + private let cancelButton = ComponentView() + private let label = ComponentView() + private let doneButton = ComponentView() + private let scrubber = ComponentView() + + private let fadeView = UIView() + + private var component: MediaCoverScreenComponent? + private weak var state: State? + private var environment: ViewControllerComponentContainer.Environment? + + override init(frame: CGRect) { + self.buttonsContainerView.clipsToBounds = true + + self.fadeView.alpha = 0.0 + self.fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.7) + + self.buttonsBackgroundView.image = generateImage(CGSize(width: 22.0, height: 22.0), rotatedContext: { size, context in + context.setFillColor(UIColor.black.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + + context.setBlendMode(.clear) + context.setFillColor(UIColor.clear.cgColor) + context.addPath(CGPath(roundedRect: CGRect(x: 0.0, y: -11.0, width: size.width, height: 22.0), cornerWidth: 11.0, cornerHeight: 11.0, transform: nil)) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 11, topCapHeight: 11) + + super.init(frame: frame) + + self.backgroundColor = .clear + + self.addSubview(self.buttonsContainerView) + self.buttonsContainerView.addSubview(self.buttonsBackgroundView) + + self.addSubview(self.fadeView) + self.addSubview(self.previewContainerView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func animateInFromEditor() { + self.buttonsBackgroundView.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.2, additive: true) + + self.label.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + if let view = self.doneButton.view { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } + } + + private var animatingOut = false + func animateOutToEditor(completion: @escaping () -> Void) { + self.animatingOut = true + + self.fadeView.layer.animateAlpha(from: self.fadeView.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.buttonsBackgroundView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.2, removeOnCompletion: false, additive: true) + + self.label.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + + if let view = self.scrubber.view { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) + } + + if let view = self.cancelButton.view { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) + } + + if let view = self.doneButton.view { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) + } + + self.state?.updated() + } + + func update(component: MediaCoverScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environment + + guard let controller = environment.controller() as? MediaCoverScreen else { + return .zero + } + + self.component = component + self.state = state + + let isTablet: Bool + if case .regular = environment.metrics.widthClass { + isTablet = true + } else { + isTablet = false + } + + let buttonSideInset: CGFloat = 16.0 + var controlsBottomInset: CGFloat = 0.0 + let previewSize: CGSize + var topInset: CGFloat = environment.statusBarHeight + 5.0 + if isTablet { + let previewHeight = availableSize.height - topInset - 75.0 + previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) + } else { + previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778)) + if availableSize.height < previewSize.height + 30.0 { + topInset = 0.0 + controlsBottomInset = -75.0 + } + } + + let previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: topInset), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom + controlsBottomInset)) + let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom + controlsBottomInset), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom - controlsBottomInset)) + + let cancelButtonSize = self.cancelButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Common_Cancel, font: Font.regular(17.0), textColor: .white))) + ), + action: { [weak controller] in + controller?.requestDismiss(animated: true) + } + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 44.0) + ) + let cancelButtonFrame = CGRect( + origin: CGPoint(x: 16.0, y: previewContainerFrame.minY + 28.0), + size: cancelButtonSize + ) + if let cancelButtonView = self.cancelButton.view { + if cancelButtonView.superview == nil { + self.addSubview(cancelButtonView) + setupButtonShadow(cancelButtonView) + } + transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame) + } + + let doneButtonSize = self.doneButton.update( + transition: transition, + component: AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(ButtonTextContentComponent( + text: environment.strings.Story_SaveCover, + badge: 0, + textColor: environment.theme.list.itemCheckColors.foregroundColor, + badgeBackground: .clear, + badgeForeground: .clear + )) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak controller, weak self] in + guard let controller else { + return + } + if let playerState = self?.state?.playerState, let mediaEditor = self?.state?.mediaEditor, let image = mediaEditor.resultImage { + mediaEditor.setCoverImageTimestamp(playerState.position) + controller.completed(playerState.position, image) + } + if !controller.exclusive { + controller.requestDismiss(animated: true) + } + } + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 50.0) + ) + let doneButtonFrame = CGRect( + origin: CGPoint(x: floor((availableSize.width - doneButtonSize.width) / 2.0), y: min(buttonsContainerFrame.minY, availableSize.height - doneButtonSize.height - buttonSideInset)), + size: doneButtonSize + ) + if let doneButtonView = self.doneButton.view { + if doneButtonView.superview == nil { + self.addSubview(doneButtonView) + } + transition.setFrame(view: doneButtonView, frame: doneButtonFrame) + } + + let labelSize = self.label.update( + transition: transition, + component: AnyComponent(Text(text: environment.strings.Story_Cover, font: Font.semibold(17.0), color: UIColor(rgb: 0xffffff))), + environment: {}, + containerSize: CGSize(width: availableSize.width - 88.0, height: 44.0) + ) + let labelFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels((availableSize.width - labelSize.width) / 2.0), y: previewContainerFrame.minY + 28.0), + size: labelSize + ) + if let labelView = self.label.view { + if labelView.superview == nil { + self.addSubview(labelView) + setupButtonShadow(labelView) + } + if labelView.bounds.width > 0.0 && labelFrame.width != labelView.bounds.width { + if let snapshotView = labelView.snapshotView(afterScreenUpdates: false) { + snapshotView.center = labelView.center + self.buttonsContainerView.addSubview(snapshotView) + + labelView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + } + } + labelView.bounds = CGRect(origin: .zero, size: labelFrame.size) + transition.setPosition(view: labelView, position: labelFrame.center) + } + + let buttonCoverFrame = CGRect(origin: CGPoint(x: 0.0, y: doneButtonFrame.minY - buttonSideInset - 11.0), size: CGSize(width: previewContainerFrame.width, height: 100.0)) + + transition.setFrame(view: self.buttonsContainerView, frame: buttonCoverFrame) + transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonCoverFrame.size)) + + transition.setFrame(view: self.previewContainerView, frame: previewContainerFrame) + + if let playerState = state.playerState { + let visibleTracks = playerState.tracks.filter { $0.id == 0 }.map { MediaScrubberComponent.Track($0) } + + let scrubberInset: CGFloat = buttonSideInset + let scrubberSize = self.scrubber.update( + transition: transition, + component: AnyComponent(MediaScrubberComponent( + context: component.context, + style: .cover, + theme: environment.theme, + generationTimestamp: playerState.generationTimestamp, + position: playerState.position, + minDuration: 1.0, + maxDuration: storyMaxVideoDuration, + isPlaying: playerState.isPlaying, + tracks: visibleTracks, + portalView: controller.portalView, + positionUpdated: { [weak state] position, apply in + if let mediaEditor = state?.mediaEditor { + mediaEditor.seek(position, andPlay: false) + } + }, + coverPositionUpdated: { [weak state] position, tap, commit in + if let mediaEditor = state?.mediaEditor { + if tap { + mediaEditor.setOnNextDisplay { + commit() + } + mediaEditor.seek(position, andPlay: false) + } else { + mediaEditor.seek(position, andPlay: false) + commit() + } + } + }, + trackTrimUpdated: { _, _, _, _, _ in + }, + trackOffsetUpdated: { _, _, _ in + }, + trackLongPressed: { _, _ in + } + )), + environment: {}, + containerSize: CGSize(width: previewSize.width - scrubberInset * 2.0, height: availableSize.height) + ) + + let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: min(previewContainerFrame.maxY, buttonCoverFrame.minY) - scrubberSize.height - 4.0), size: scrubberSize) + if let scrubberView = self.scrubber.view { + var animateIn = false + if scrubberView.superview == nil { + animateIn = true + self.addSubview(scrubberView) + } + if animateIn { + scrubberView.frame = scrubberFrame + } else { + transition.setFrame(view: scrubberView, frame: scrubberFrame) + } + if animateIn { + scrubberView.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + scrubberView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + scrubberView.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2) + } + } + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class MediaCoverScreen: ViewController { + fileprivate final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate { + private weak var controller: MediaCoverScreen? + private let context: AccountContext + + fileprivate let componentHost: ComponentView + + private var presentationData: PresentationData + private var validLayout: ContainerViewLayout? + + init(controller: MediaCoverScreen) { + self.controller = controller + self.context = controller.context + + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + self.componentHost = ComponentView() + + super.init() + + self.backgroundColor = .clear + } + + override func didLoad() { + super.didLoad() + + self.view.disablesInteractiveModalDismiss = true + self.view.disablesInteractiveKeyboardGestureRecognizer = true + } + + @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func animateInFromEditor() { + if let view = self.componentHost.view as? MediaCoverScreenComponent.View { + view.animateInFromEditor() + } + } + + func animateOutToEditor(completion: @escaping () -> Void) { + if let view = self.componentHost.view as? MediaCoverScreenComponent.View { + view.animateOutToEditor(completion: completion) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result === self.view { + return nil + } + return result + } + + func requestLayout(transition: ComponentTransition) { + if let layout = self.validLayout { + self.containerLayoutUpdated(layout: layout, forceUpdate: true, transition: transition) + } + } + + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: ComponentTransition) { + guard let controller = self.controller else { + return + } + let isFirstTime = self.validLayout == nil + self.validLayout = layout + + let isTablet = layout.metrics.isTablet + + let previewSize: CGSize + let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0 + if isTablet { + let previewHeight = layout.size.height - topInset - 75.0 + previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) + } else { + previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) + } + let bottomInset = layout.size.height - previewSize.height - topInset + + let environment = ViewControllerComponentContainer.Environment( + statusBarHeight: layout.statusBarHeight ?? 0.0, + navigationHeight: 0.0, + safeInsets: UIEdgeInsets( + top: topInset, + left: layout.safeInsets.left, + bottom: bottomInset, + right: layout.safeInsets.right + ), + additionalInsets: layout.additionalInsets, + inputHeight: layout.inputHeight ?? 0.0, + metrics: layout.metrics, + deviceMetrics: layout.deviceMetrics, + orientation: nil, + isVisible: true, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + controller: { [weak self] in + return self?.controller + } + ) + + let componentSize = self.componentHost.update( + transition: transition, + component: AnyComponent( + MediaCoverScreenComponent( + context: self.context, + mediaEditor: controller.mediaEditor, + exclusive: controller.exclusive + ) + ), + environment: { + environment + }, + forceUpdate: forceUpdate || animateOut, + containerSize: layout.size + ) + if let componentView = self.componentHost.view { + if componentView.superview == nil { + self.view.insertSubview(componentView, at: 3) + componentView.clipsToBounds = true + } + let componentFrame = CGRect(origin: .zero, size: componentSize) + transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) + } + + if isFirstTime { + self.animateInFromEditor() + } + } + } + + fileprivate var node: Node { + return self.displayNode as! Node + } + + fileprivate let context: AccountContext + fileprivate let mediaEditor: Signal + fileprivate let previewView: MediaEditorPreviewView + fileprivate let portalView: PortalView + fileprivate let exclusive: Bool + + func withMediaEditor(_ f: @escaping (MediaEditor) -> Void) { + let _ = (self.mediaEditor + |> take(1) + |> deliverOnMainQueue).start(next: { mediaEditor in + if let mediaEditor { + f(mediaEditor) + } + }) + } + + var completed: (Double, UIImage) -> Void = { _, _ in } + var dismissed: () -> Void = {} + + init( + context: AccountContext, + mediaEditor: Signal, + previewView: MediaEditorPreviewView, + portalView: PortalView, + exclusive: Bool + ) { + self.context = context + self.mediaEditor = mediaEditor + self.previewView = previewView + self.portalView = portalView + self.exclusive = exclusive + + super.init(navigationBarPresentationData: nil) + self.navigationPresentation = .flatModal + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.statusBar.statusBarStyle = .White + + self.withMediaEditor { mediaEditor in + if let coverImageTimestamp = mediaEditor.values.coverImageTimestamp { + mediaEditor.seek(coverImageTimestamp, andPlay: false) + } else { + mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: false) + } + } + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + self.displayNode = Node(controller: self) + + super.displayNodeDidLoad() + } + + func requestDismiss(animated: Bool) { + self.dismissed() + + self.node.animateOutToEditor(completion: { + if !self.exclusive { + self.dismiss() + } + }) + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) + } +} + +private func setupButtonShadow(_ view: UIView, radius: CGFloat = 2.0) { + view.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) + view.layer.shadowRadius = radius + view.layer.shadowColor = UIColor.black.cgColor + view.layer.shadowOpacity = 0.35 +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift index b5c74027ad8..d29a7083b95 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift @@ -46,7 +46,7 @@ extension MediaEditorScreen { return true } - func saveDraft(id: Int64?) { + func saveDraft(id: Int64?, edit: Bool = false) { guard let subject = self.node.subject, let actualSubject = self.node.actualSubject, let mediaEditor = self.node.mediaEditor else { return } @@ -83,7 +83,9 @@ extension MediaEditorScreen { } if let resultImage = mediaEditor.resultImage { - mediaEditor.seek(0.0, andPlay: false) + if !edit { + mediaEditor.seek(0.0, andPlay: false) + } makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: resultImage, dimensions: storyDimensions, values: values, time: .zero, textScale: 2.0, completion: { resultImage in guard let resultImage else { return diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index c6758ebf516..ff60dcfd7d1 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -48,6 +48,7 @@ import StickerPackEditTitleController import StickerPickerScreen import UIKitRuntimeUtils import ImageObjectSeparation +import SaveProgressScreen private let playbackButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag() @@ -76,6 +77,7 @@ final class MediaEditorScreenComponent: Component { case cutout case cutoutErase case cutoutRestore + case cover } let context: AccountContext @@ -731,7 +733,7 @@ final class MediaEditorScreenComponent: Component { transition = transition.withUserData(nextTransitionUserData) } - let isEditingStory = controller.isEditingStory + let isEditingStory = controller.isEditingStory || controller.isEditingStoryCover if self.component == nil { if let initialCaption = controller.initialCaption { self.inputPanelExternalState.initialText = initialCaption @@ -818,7 +820,7 @@ final class MediaEditorScreenComponent: Component { } var doneButtonTitle: String? - var doneButtonIcon: UIImage + var doneButtonIcon: UIImage? switch controller.mode { case .storyEditor: doneButtonTitle = isEditingStory ? environment.strings.Story_Editor_Done.uppercased() : environment.strings.Story_Editor_Next.uppercased() @@ -826,6 +828,9 @@ final class MediaEditorScreenComponent: Component { case .stickerEditor: doneButtonTitle = nil doneButtonIcon = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Apply"), color: .white)! + case .botPreview: + doneButtonTitle = environment.strings.Story_Editor_Add.uppercased() + doneButtonIcon = nil } let doneButtonSize = self.doneButton.update( @@ -837,29 +842,7 @@ final class MediaEditorScreenComponent: Component { title: doneButtonTitle)), effectAlignment: .center, action: { [weak controller] in - guard let controller else { - return - } - switch controller.mode { - case .storyEditor: - guard !controller.node.recording.isActive else { - return - } - guard controller.checkCaptionLimit() else { - return - } - if controller.isEditingStory { - controller.requestStoryCompletion(animated: true) - } else { - if controller.checkIfCompletionIsAllowed() { - controller.openPrivacySettings(completion: { [weak controller] in - controller?.requestStoryCompletion(animated: true) - }) - } - } - case .stickerEditor: - controller.requestStickerCompletion(animated: true) - } + controller?.node.requestCompletion() } )), environment: {}, @@ -1216,6 +1199,7 @@ final class MediaEditorScreenComponent: Component { let displayTopButtons = !(self.inputPanelExternalState.isEditing || isEditingTextEntity || component.isDisplayingTool != nil) + var inputPanelSize: CGSize = .zero if case .storyEditor = controller.mode { let nextInputMode: MessageInputPanelComponent.InputMode switch self.currentInputMode { @@ -1235,7 +1219,7 @@ final class MediaEditorScreenComponent: Component { } self.inputPanel.parentState = state - let inputPanelSize = self.inputPanel.update( + inputPanelSize = self.inputPanel.update( transition: transition, component: AnyComponent(MessageInputPanelComponent( externalState: self.inputPanelExternalState, @@ -1447,192 +1431,7 @@ final class MediaEditorScreenComponent: Component { transition.setFrame(view: inputPanelView, frame: inputPanelFrame) transition.setAlpha(view: inputPanelView, alpha: isEditingTextEntity || component.isDisplayingTool != nil || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } - - if let playerState = state.playerState { - let scrubberInset: CGFloat = 9.0 - - let minDuration: Double - let maxDuration: Double - if playerState.isAudioOnly { - minDuration = 5.0 - maxDuration = 15.0 - } else { - minDuration = 1.0 - maxDuration = storyMaxVideoDuration - } - - let previousTrackCount = self.currentVisibleTracks?.count - let visibleTracks = playerState.tracks.filter { $0.visibleInTimeline }.map { MediaScrubberComponent.Track($0) } - self.currentVisibleTracks = visibleTracks - - var scrubberTransition = transition - if let previousTrackCount, previousTrackCount != visibleTracks.count { - scrubberTransition = .easeInOut(duration: 0.2) - } - - let isAudioOnly = playerState.isAudioOnly - let hasMainVideoTrack = playerState.tracks.contains(where: { $0.id == 0 }) - - let scrubber: ComponentView - if let current = self.scrubber { - scrubber = current - } else { - scrubber = ComponentView() - self.scrubber = scrubber - } - - let scrubberSize = scrubber.update( - transition: scrubberTransition, - component: AnyComponent(MediaScrubberComponent( - context: component.context, - style: .editor, - theme: environment.theme, - generationTimestamp: playerState.generationTimestamp, - position: playerState.position, - minDuration: minDuration, - maxDuration: maxDuration, - isPlaying: playerState.isPlaying, - tracks: visibleTracks, - positionUpdated: { [weak mediaEditor] position, apply in - if let mediaEditor { - mediaEditor.seek(position, andPlay: apply) - } - }, - trackTrimUpdated: { [weak mediaEditor] trackId, start, end, updatedEnd, apply in - guard let mediaEditor else { - return - } - let trimRange = start..= upperBound { - start = lowerBound - } else if start < lowerBound { - start = lowerBound - } - } - - mediaEditor.seek(start, andPlay: true) - mediaEditor.play() - } else { - mediaEditor.stop() - } - } - } else if trackId == 1 { - mediaEditor.setAdditionalVideoOffset(offset, apply: apply) - } - }, - trackLongPressed: { [weak controller] trackId, sourceView in - guard let controller else { - return - } - controller.node.presentTrackOptions(trackId: trackId, sourceView: sourceView) - } - )), - environment: {}, - containerSize: CGSize(width: previewSize.width - scrubberInset * 2.0, height: availableSize.height) - ) - - let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height + controlsBottomInset - inputPanelSize.height + 3.0), size: scrubberSize) - if let scrubberView = scrubber.view { - var animateIn = false - if scrubberView.superview == nil { - animateIn = true - if let inputPanelBackgroundView = self.inputPanelBackground.view, inputPanelBackgroundView.superview != nil { - self.insertSubview(scrubberView, belowSubview: inputPanelBackgroundView) - } else { - self.addSubview(scrubberView) - } - } - if animateIn { - scrubberView.frame = scrubberFrame - } else { - scrubberTransition.setFrame(view: scrubberView, frame: scrubberFrame) - } - if !self.animatingButtons && !(!hasMainVideoTrack && animateIn) { - transition.setAlpha(view: scrubberView, alpha: component.isDisplayingTool != nil || component.isDismissing || component.isInteractingWithEntities || isEditingCaption || isRecordingAdditionalVideo || isEditingTextEntity ? 0.0 : 1.0) - } else if animateIn { - scrubberView.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - scrubberView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - scrubberView.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2) - } - } - } else { - if let scrubber = self.scrubber { - self.scrubber = nil - if let scrubberView = scrubber.view { - scrubberView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - scrubberView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in - scrubberView.removeFromSuperview() - }) - scrubberView.layer.animateScale(from: 1.0, to: 0.6, duration: 0.2, removeOnCompletion: false) - } - } - } - + let saveContentComponent: AnyComponentWithIdentity if component.hasAppeared { saveContentComponent = AnyComponentWithIdentity( @@ -1941,56 +1740,247 @@ final class MediaEditorScreenComponent: Component { playbackButtonView.layer.animateAlpha(from: 0.0, to: playbackButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2) playbackButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2) } - transition.setPosition(view: playbackButtonView, position: playbackButtonFrame.center) - transition.setBounds(view: playbackButtonView, bounds: CGRect(origin: .zero, size: playbackButtonFrame.size)) - transition.setScale(view: playbackButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: playbackButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) + transition.setPosition(view: playbackButtonView, position: playbackButtonFrame.center) + transition.setBounds(view: playbackButtonView, bounds: CGRect(origin: .zero, size: playbackButtonFrame.size)) + transition.setScale(view: playbackButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: playbackButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) + } + + topButtonOffsetX += 50.0 + } else { + if let playbackButtonView = self.playbackButton.view, playbackButtonView.superview != nil { + playbackButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak playbackButtonView] _ in + playbackButtonView?.removeFromSuperview() + }) + playbackButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } + } + + let switchCameraButtonSize = self.switchCameraButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent( + FlipButtonContentComponent(tag: switchCameraButtonTag) + ), + action: { [weak self, weak controller] in + if let self, let controller { + controller.node.recording.togglePosition() + + if let view = self.switchCameraButton.findTaggedView(tag: switchCameraButtonTag) as? FlipButtonContentComponent.View { + view.playAnimation() + } + } + } + ).withIsExclusive(false)), + environment: {}, + containerSize: CGSize(width: 48.0, height: 48.0) + ) + let switchCameraButtonFrame = CGRect( + origin: CGPoint(x: 12.0, y: max(environment.statusBarHeight + 10.0, inputPanelFrame.minY - switchCameraButtonSize.height - 3.0)), + size: switchCameraButtonSize + ) + if let switchCameraButtonView = self.switchCameraButton.view { + if switchCameraButtonView.superview == nil { + self.addSubview(switchCameraButtonView) + } + transition.setPosition(view: switchCameraButtonView, position: switchCameraButtonFrame.center) + transition.setBounds(view: switchCameraButtonView, bounds: CGRect(origin: .zero, size: switchCameraButtonFrame.size)) + transition.setScale(view: switchCameraButtonView, scale: isRecordingAdditionalVideo ? 1.0 : 0.01) + transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0) + } + } else { + inputPanelSize = CGSize(width: 0.0, height: 12.0) + } + + if case .stickerEditor = controller.mode { + + } else { + if let playerState = state.playerState { + let scrubberInset: CGFloat = 9.0 + + let minDuration: Double + let maxDuration: Double + if playerState.isAudioOnly { + minDuration = 5.0 + maxDuration = 15.0 + } else { + minDuration = 1.0 + maxDuration = storyMaxVideoDuration + } + + let previousTrackCount = self.currentVisibleTracks?.count + let visibleTracks = playerState.tracks.filter { $0.visibleInTimeline }.map { MediaScrubberComponent.Track($0) } + self.currentVisibleTracks = visibleTracks + + var scrubberTransition = transition + if let previousTrackCount, previousTrackCount != visibleTracks.count { + scrubberTransition = .easeInOut(duration: 0.2) + } + + let isAudioOnly = playerState.isAudioOnly + let hasMainVideoTrack = playerState.tracks.contains(where: { $0.id == 0 }) + + let scrubber: ComponentView + if let current = self.scrubber { + scrubber = current + } else { + scrubber = ComponentView() + self.scrubber = scrubber + } + + let scrubberSize = scrubber.update( + transition: scrubberTransition, + component: AnyComponent(MediaScrubberComponent( + context: component.context, + style: .editor, + theme: environment.theme, + generationTimestamp: playerState.generationTimestamp, + position: playerState.position, + minDuration: minDuration, + maxDuration: maxDuration, + isPlaying: playerState.isPlaying, + tracks: visibleTracks, + positionUpdated: { [weak mediaEditor] position, apply in + if let mediaEditor { + mediaEditor.seek(position, andPlay: apply) + } + }, + trackTrimUpdated: { [weak mediaEditor] trackId, start, end, updatedEnd, apply in + guard let mediaEditor else { + return + } + let trimRange = start..= upperBound { + start = lowerBound + } else if start < lowerBound { + start = lowerBound + } + } + + mediaEditor.seek(start, andPlay: true) + mediaEditor.play() + } else { + mediaEditor.stop() + } + } + } else if trackId == 1 { + mediaEditor.setAdditionalVideoOffset(offset, apply: apply) + } + }, + trackLongPressed: { [weak controller] trackId, sourceView in + guard let controller else { + return + } + controller.node.presentTrackOptions(trackId: trackId, sourceView: sourceView) + } + )), + environment: {}, + containerSize: CGSize(width: previewSize.width - scrubberInset * 2.0, height: availableSize.height) + ) + + let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height + controlsBottomInset - inputPanelSize.height + 3.0), size: scrubberSize) + if let scrubberView = scrubber.view { + var animateIn = false + if scrubberView.superview == nil { + animateIn = true + if let inputPanelBackgroundView = self.inputPanelBackground.view, inputPanelBackgroundView.superview != nil { + self.insertSubview(scrubberView, belowSubview: inputPanelBackgroundView) + } else { + self.addSubview(scrubberView) + } + } + if animateIn { + scrubberView.frame = scrubberFrame + } else { + scrubberTransition.setFrame(view: scrubberView, frame: scrubberFrame) + } + if !self.animatingButtons && !(!hasMainVideoTrack && animateIn) { + transition.setAlpha(view: scrubberView, alpha: component.isDisplayingTool != nil || component.isDismissing || component.isInteractingWithEntities || isEditingCaption || isRecordingAdditionalVideo || isEditingTextEntity ? 0.0 : 1.0) + } else if animateIn { + scrubberView.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + scrubberView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + scrubberView.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2) + } } - - topButtonOffsetX += 50.0 } else { - if let playbackButtonView = self.playbackButton.view, playbackButtonView.superview != nil { - playbackButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak playbackButtonView] _ in - playbackButtonView?.removeFromSuperview() - }) - playbackButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - } - } - - let switchCameraButtonSize = self.switchCameraButton.update( - transition: transition, - component: AnyComponent(Button( - content: AnyComponent( - FlipButtonContentComponent(tag: switchCameraButtonTag) - ), - action: { [weak self, weak controller] in - if let self, let controller { - controller.node.recording.togglePosition() - - if let view = self.switchCameraButton.findTaggedView(tag: switchCameraButtonTag) as? FlipButtonContentComponent.View { - view.playAnimation() - } - } + if let scrubber = self.scrubber { + self.scrubber = nil + if let scrubberView = scrubber.view { + scrubberView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + scrubberView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + scrubberView.removeFromSuperview() + }) + scrubberView.layer.animateScale(from: 1.0, to: 0.6, duration: 0.2, removeOnCompletion: false) } - ).withIsExclusive(false)), - environment: {}, - containerSize: CGSize(width: 48.0, height: 48.0) - ) - let switchCameraButtonFrame = CGRect( - origin: CGPoint(x: 12.0, y: max(environment.statusBarHeight + 10.0, inputPanelFrame.minY - switchCameraButtonSize.height - 3.0)), - size: switchCameraButtonSize - ) - if let switchCameraButtonView = self.switchCameraButton.view { - if switchCameraButtonView.superview == nil { - self.addSubview(switchCameraButtonView) } - transition.setPosition(view: switchCameraButtonView, position: switchCameraButtonFrame.center) - transition.setBounds(view: switchCameraButtonView, bounds: CGRect(origin: .zero, size: switchCameraButtonFrame.size)) - transition.setScale(view: switchCameraButtonView, scale: isRecordingAdditionalVideo ? 1.0 : 0.01) - transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0) } - } + if case .stickerEditor = controller.mode { var stickerButtonsHidden = buttonsAreHidden if let displayingTool = component.isDisplayingTool, [.cutoutErase, .cutoutRestore].contains(displayingTool) { @@ -2415,6 +2405,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate case storyEditor case stickerEditor(mode: StickerEditorMode) + case botPreview } public enum TransitionIn { @@ -2443,15 +2434,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate public weak var destinationView: UIView? public let destinationRect: CGRect public let destinationCornerRadius: CGFloat + public let completion: (() -> Void)? public init( destinationView: UIView, destinationRect: CGRect, - destinationCornerRadius: CGFloat + destinationCornerRadius: CGFloat, + completion: (() -> Void)? = nil ) { self.destinationView = destinationView self.destinationRect = destinationRect self.destinationCornerRadius = destinationCornerRadius + self.completion = completion } } @@ -2545,10 +2539,23 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private(set) var hasAnyChanges = false + fileprivate var drawingScreen: DrawingScreen? + fileprivate var stickerScreen: StickerPickerScreen? + fileprivate weak var cutoutScreen: MediaCutoutScreen? + fileprivate weak var coverScreen: MediaCoverScreen? + private var defaultToEmoji = false + + private var previousDrawingData: Data? + private var previousDrawingEntities: [DrawingEntity]? + + private var weatherPromise: Promise? + private var playbackPositionDisposable: Disposable? var recording: MediaEditorScreen.Recording + private let locationManager = LocationManager() + private var presentationData: PresentationData private var validLayout: ContainerViewLayout? @@ -2801,6 +2808,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.availableReactions = reactions } }) + + if controller.isEditingStoryCover { + Queue.mainQueue().justDispatch { + self.openCoverSelection(exclusive: true) + } + } } deinit { @@ -2860,7 +2873,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let mediaEntity = DrawingMediaEntity(size: fittedSize) mediaEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) switch controller.mode { - case .storyEditor: + case .storyEditor, .botPreview: if fittedSize.height > fittedSize.width { mediaEntity.scale = max(storyDimensions.width / fittedSize.width, storyDimensions.height / fittedSize.height) } else { @@ -2881,6 +2894,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if isFromCamera && mediaDimensions.width > mediaDimensions.height { mediaEntity.scale = storyDimensions.height / fittedSize.height } + + if case .botPreview = controller.mode { + if fittedSize.width / fittedSize.height < storyDimensions.width / storyDimensions.height { + mediaEntity.scale = storyDimensions.height / fittedSize.height + } + } let initialValues: MediaEditorValues? if case let .draft(draft, _) = subject { @@ -2926,7 +2945,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let mediaEditor = MediaEditor(context: self.context, mode: isStickerEditor ? .sticker : .default, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true) if let initialVideoPosition = controller.initialVideoPosition { - mediaEditor.seek(initialVideoPosition, andPlay: true) + if controller.isEditingStoryCover { + mediaEditor.setCoverImageTimestamp(initialVideoPosition) + } else { + mediaEditor.seek(initialVideoPosition, andPlay: true) + } } if case .message = subject, self.context.sharedContext.currentPresentationData.with({$0}).autoNightModeTriggered { mediaEditor.setNightTheme(true) @@ -2968,7 +2991,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } self.controller?.stickerRecommendedEmoji = emojiForClasses(classes.map { $0.0 }) } - mediaEditor.attachPreviewView(self.previewView) + mediaEditor.attachPreviewView(self.previewView, andPlay: !(self.controller?.isEditingStoryCover ?? false)) if case .empty = effectiveSubject { self.stickerMaskDrawingView?.emptyColor = .black @@ -3143,6 +3166,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } } + + if let initialLink = controller.initialLink { + self.addInitialLink(initialLink) + } } private var initialMaskScale: CGFloat = .zero @@ -3613,7 +3640,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate transitionInView.contentMode = .scaleAspectFill var initialScale: CGFloat switch controller.mode { - case .storyEditor: + case .storyEditor, .botPreview: if image.size.height > image.size.width { initialScale = max(self.previewContainerView.bounds.width / image.size.width, self.previewContainerView.bounds.height / image.size.height) } else { @@ -3775,6 +3802,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let transitionOut = controller.transitionOut(finished, isNew), let destinationView = transitionOut.destinationView { var destinationTransitionView: UIView? var destinationTransitionRect: CGRect = .zero + let transitionOutCompletion = transitionOut.completion if !finished { if let transitionIn = controller.transitionIn, case let .gallery(galleryTransitionIn) = transitionIn, let sourceImage = galleryTransitionIn.sourceImage, isNew != true { let sourceSuperView = galleryTransitionIn.sourceView?.superview?.superview @@ -3857,6 +3885,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate destinationView.isHidden = false destinationSnapshotView?.removeFromSuperview() completion() + transitionOutCompletion?() }) self.previewContainerView.layer.animateScale(from: 1.0, to: destinationScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width * destinationAspectRatio) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width * destinationAspectRatio)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) @@ -4155,6 +4184,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } if !self.didSetupStaticEmojiPack { + self.didSetupStaticEmojiPack = true self.staticEmojiPack.set(self.context.engine.stickers.loadedStickerPack(reference: .name("staticemoji"), forceActualized: false)) } @@ -4212,7 +4242,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate emojiFile = .single(nil) } - let _ = emojiFile.start(next: { [weak self] emojiFile in + let _ = (emojiFile + |> deliverOnMainQueue).start(next: { [weak self] emojiFile in guard let self else { return } @@ -4543,6 +4574,32 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate controller.push(linkController) } + func addInitialLink(_ link: (url: String, name: String?)) { + guard self.context.isPremium else { + Queue.mainQueue().after(0.3) { + let context = self.context + var replaceImpl: ((ViewController) -> Void)? + let demoController = context.sharedContext.makePremiumDemoController(context: context, subject: .stories, forceDark: true, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesLinks, forceDark: true, dismissed: {}) + replaceImpl?(controller) + }, dismissed: {}) + replaceImpl = { [weak self, weak demoController] c in + demoController?.dismiss(animated: true, completion: { + guard let self else { + return + } + self.controller?.push(c) + }) + } + self.controller?.push(demoController) + } + return + } + + let entity = DrawingLinkEntity(url: link.url, name: link.name ?? "", webpage: nil, positionBelowText: false, largeMedia: nil, style: .white) + self.interaction?.insertEntity(entity, position: CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.width / 3.0 * 4.0), select: false) + } + func addReaction() { guard let controller = self.controller else { return @@ -4570,6 +4627,139 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.mediaEditor?.play() } + func requestWeather() { + + } + + func presentLocationAccessAlert() { + DeviceAccess.authorizeAccess(to: .location(.weather), locationManager: self.locationManager, presentationData: self.presentationData, present: { [weak self] c, a in + self?.controller?.present(c, in: .window(.root), with: a) + }, openSettings: { [weak self] in + self?.context.sharedContext.applicationBindings.openSettings() + }, { [weak self] authorized in + guard let self, authorized else { + return + } + let weatherPromise = Promise() + weatherPromise.set(getWeather(context: self.context, load: true)) + self.weatherPromise = weatherPromise + + let _ = (weatherPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] result in + if let self, case let .loaded(weather) = result { + self.addWeather(weather) + } + }) + }) + } + + func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather?) { + guard let weather else { + + return + } + let maxWeatherCount = 1 + var currentWeatherCount = 0 + self.entitiesView.eachView { entityView in + if entityView.entity is DrawingWeatherEntity { + currentWeatherCount += 1 + } + } + if currentWeatherCount >= maxWeatherCount { + self.controller?.presentWeatherLimitTooltip() + return + } + + self.interaction?.insertEntity( + DrawingWeatherEntity( + emoji: weather.emoji, + emojiFile: weather.emojiFile, + temperature: weather.temperature, + style: .white + ), + scale: nil, + position: nil + ) + } + + func requestCompletion(playHaptic: Bool = true) { + guard let controller = self.controller else { + return + } + switch controller.mode { + case .storyEditor: + guard !controller.node.recording.isActive else { + return + } + guard controller.checkCaptionLimit() else { + return + } + if controller.isEditingStory || controller.isEditingStoryCover { + controller.requestStoryCompletion(animated: true) + } else { + if controller.checkIfCompletionIsAllowed() { + controller.hapticFeedback.impact(.light) + controller.openPrivacySettings(completion: { [weak controller] in + controller?.requestStoryCompletion(animated: true) + }) + } + } + case .stickerEditor: + controller.requestStickerCompletion(animated: true) + case .botPreview: + controller.requestStoryCompletion(animated: true) + } + } + + func openCoverSelection(exclusive: Bool) { + guard let portalView = PortalView(matchPosition: false) else { + return + } + portalView.view.layer.rasterizationScale = UIScreenScale + self.previewContentContainerView.addPortal(view: portalView) + + let scale = 48.0 / self.previewContentContainerView.frame.height + portalView.view.transform = CGAffineTransformMakeScale(scale, scale) + + if self.entitiesView.hasSelection { + self.entitiesView.selectEntity(nil) + } + let coverController = MediaCoverScreen( + context: self.context, + mediaEditor: self.mediaEditorPromise.get(), + previewView: self.previewView, + portalView: portalView, + exclusive: exclusive + ) + coverController.dismissed = { [weak self] in + if let self { + if exclusive { + self.controller?.requestDismiss(saveDraft: false, animated: true) + } else { + self.animateInFromTool() + self.requestCompletion(playHaptic: false) + } + } + } + coverController.completed = { [weak self] position, image in + if let self { + self.controller?.currentCoverImage = image + if exclusive { + self.requestCompletion() + } + } + } + self.controller?.present(coverController, in: .current) + self.coverScreen = coverController + + if exclusive { + self.isDisplayingTool = .cover + self.requestUpdate(transition: .immediate) + } else { + self.animateOutToTool(tool: .cover) + } + } + func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) { guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else { return @@ -4648,15 +4838,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate func viewForZooming(in scrollView: UIScrollView) -> UIView? { return self.previewContentContainerView } - - fileprivate var drawingScreen: DrawingScreen? - fileprivate var stickerScreen: StickerPickerScreen? - fileprivate weak var cutoutScreen: MediaCutoutScreen? - private var defaultToEmoji = false - - private var previousDrawingData: Data? - private var previousDrawingEntities: [DrawingEntity]? - + func requestLayout(forceUpdate: Bool, transition: ComponentTransition) { guard let layout = self.validLayout else { return @@ -4750,10 +4932,33 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate mediaEditor.maybePauseVideo() var hasInteractiveStickers = true - if let controller = self.controller, case .stickerEditor = controller.mode { - hasInteractiveStickers = false + if let controller = self.controller { + switch controller.mode { + case .stickerEditor, .botPreview: + hasInteractiveStickers = false + default: + break + } + } + + let editorConfiguration = MediaEditorConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) + + var weatherSignal: Signal + if hasInteractiveStickers { + let weatherPromise: Promise + if let current = self.weatherPromise { + weatherPromise = current + } else { + weatherPromise = Promise() + weatherPromise.set(getWeather(context: self.context, load: editorConfiguration.preloadWeather)) + self.weatherPromise = weatherPromise + } + weatherSignal = weatherPromise.get() + } else { + weatherSignal = .single(.none) } - let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), forceDark: true, defaultToEmoji: self.defaultToEmoji, hasGifs: true, hasInteractiveStickers: hasInteractiveStickers) + + let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), forceDark: true, defaultToEmoji: self.defaultToEmoji, hasGifs: true, hasInteractiveStickers: hasInteractiveStickers, weather: weatherSignal) controller.completion = { [weak self] content in guard let self else { return false @@ -4824,6 +5029,36 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate controller?.dismiss(animated: true) } } + controller.addWeather = { [weak self, weak controller] in + if let self { + if let weatherPromise = self.weatherPromise { + let _ = (weatherPromise.get() + |> take(1)).start(next: { [weak self] result in + if let self { + switch result { + case let .loaded(weather): + self.addWeather(weather) + case .notPreloaded: + weatherPromise.set(getWeather(context: self.context, load: true)) + let _ = (weatherPromise.get() + |> take(1)).start(next: { [weak self] result in + if let self, case let .loaded(weather) = result { + self.addWeather(weather) + } + }) + case .notDetermined, .notAllowed: + self.presentLocationAccessAlert() + default: + break + } + } + }) + } + + self.stickerScreen = nil + controller?.dismiss(animated: true) + } + } controller.pushController = { [weak self] c in self?.controller?.push(c) } @@ -5005,6 +5240,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } self.controller?.present(controller, in: .window(.root)) self.animateOutToTool(tool: .tools) + case .cover: + self.openCoverSelection(exclusive: false) } } }, @@ -5318,6 +5555,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate public let media: MediaResult? public let mediaAreas: [MediaArea] public let caption: NSAttributedString + public let coverTimestamp: Double? public let options: MediaEditorResultPrivacy public let stickers: [TelegramMediaFile] public let randomId: Int64 @@ -5326,6 +5564,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.media = nil self.mediaAreas = [] self.caption = NSAttributedString() + self.coverTimestamp = nil self.options = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false) self.stickers = [] self.randomId = 0 @@ -5335,6 +5574,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate media: MediaResult?, mediaAreas: [MediaArea] = [], caption: NSAttributedString = NSAttributedString(), + coverTimestamp: Double? = nil, options: MediaEditorResultPrivacy = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false), stickers: [TelegramMediaFile] = [], randomId: Int64 = 0 @@ -5342,6 +5582,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.media = media self.mediaAreas = mediaAreas self.caption = caption + self.coverTimestamp = coverTimestamp self.options = options self.stickers = stickers self.randomId = randomId @@ -5352,6 +5593,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let mode: Mode let subject: Signal let isEditingStory: Bool + let isEditingStoryCover: Bool fileprivate let customTarget: EnginePeer.Id? let forwardSource: (EnginePeer, EngineStoryItem)? @@ -5359,6 +5601,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate fileprivate let initialPrivacy: EngineStoryPrivacy? fileprivate let initialMediaAreas: [MediaArea]? fileprivate let initialVideoPosition: Double? + fileprivate let initialLink: (url: String, name: String?)? fileprivate let transitionIn: TransitionIn? fileprivate let transitionOut: (Bool, Bool?) -> TransitionOut? @@ -5388,11 +5631,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate subject: Signal, customTarget: EnginePeer.Id? = nil, isEditing: Bool = false, + isEditingCover: Bool = false, forwardSource: (EnginePeer, EngineStoryItem)? = nil, initialCaption: NSAttributedString? = nil, initialPrivacy: EngineStoryPrivacy? = nil, initialMediaAreas: [MediaArea]? = nil, initialVideoPosition: Double? = nil, + initialLink: (url: String, name: String?)? = nil, transitionIn: TransitionIn?, transitionOut: @escaping (Bool, Bool?) -> TransitionOut?, completion: @escaping (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void @@ -5402,11 +5647,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.subject = subject self.customTarget = customTarget self.isEditingStory = isEditing + self.isEditingStoryCover = isEditingCover self.forwardSource = forwardSource self.initialCaption = initialCaption self.initialPrivacy = initialPrivacy self.initialMediaAreas = initialMediaAreas self.initialVideoPosition = initialVideoPosition + self.initialLink = initialLink self.transitionIn = transitionIn self.transitionOut = transitionOut self.completion = completion @@ -5577,19 +5824,29 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } fileprivate var isEmbeddedEditor: Bool { - return self.isEditingStory || self.forwardSource != nil + return self.isEditingStory || self.isEditingStoryCover || self.forwardSource != nil } + private var currentCoverImage: UIImage? func openPrivacySettings(_ privacy: MediaEditorResultPrivacy? = nil, completion: @escaping () -> Void = {}) { - self.node.mediaEditor?.maybePauseVideo() - - self.hapticFeedback.impact(.light) - + guard let mediaEditor = self.node.mediaEditor else { + return + } + mediaEditor.maybePauseVideo() + mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: false) + let privacy = privacy ?? self.state.privacy let text = self.getCaption().string let mentions = generateTextEntities(text, enabledTypes: [.mention], currentEntities: []).map { (text as NSString).substring(with: NSRange(location: $0.range.lowerBound + 1, length: $0.range.upperBound - $0.range.lowerBound - 1)) } + let coverImage: UIImage? + if mediaEditor.sourceIsVideo { + coverImage = self.currentCoverImage ?? mediaEditor.resultImage + } else { + coverImage = nil + } + let stateContext = ShareWithPeersScreen.StateContext( context: self.context, subject: .stories(editing: false), @@ -5607,6 +5864,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let initialPrivacy = privacy.privacy let timeout = privacy.timeout + var editCoverImpl: (() -> Void)? + let controller = ShareWithPeersScreen( context: self.context, initialPrivacy: initialPrivacy, @@ -5615,6 +5874,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate pin: privacy.pin, timeout: privacy.timeout, mentions: mentions, + coverImage: coverImage, stateContext: stateContext, completion: { [weak self] sendAsPeerId, privacy, allowScreenshots, pin, _, completed in guard let self else { @@ -5664,6 +5924,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate pin: pin ), completion: completion) }) + }, + editCover: { + editCoverImpl?() } ) controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in @@ -5676,6 +5939,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.node.mediaEditor?.play() } self.push(controller) + + editCoverImpl = { [weak self] in + if let self { + self.node.openCoverSelection(exclusive: false) + } + } }) } @@ -6015,6 +6284,27 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.present(controller, in: .window(.root)) } + fileprivate func presentWeatherLimitTooltip() { + self.hapticFeedback.impact(.light) + + self.dismissAllTooltips() + + let context = self.context + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let content: UndoOverlayContent = .info( + title: nil, + text: presentationData.strings.Story_Editor_TooltipWeatherLimitText, + timeout: nil, + customUndoText: nil + ) + + let controller = UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: true, position: .top, animateInAsReplacement: false, action: { _ in + return true + }) + self.present(controller, in: .window(.root)) + } + func maybePresentDiscardAlert() { self.hapticFeedback.impact(.light) if !self.isEligibleForDraft() { @@ -6036,7 +6326,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate save = presentationData.strings.Story_Editor_DraftKeepMedia } text = presentationData.strings.Story_Editor_DraftDiscaedText - case .stickerEditor: + case .stickerEditor, .botPreview: title = presentationData.strings.Story_Editor_DraftDiscardMedia text = presentationData.strings.Story_Editor_DiscardText } @@ -6195,8 +6485,15 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - if self.isEmbeddedEditor && !(self.node.hasAnyChanges || hasEntityChanges) { - self.completion(MediaEditorScreen.Result(media: nil, mediaAreas: [], caption: caption, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in + var hasAnyChanges = self.node.hasAnyChanges + if self.isEditingStoryCover { + hasAnyChanges = false + } + + if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) { + self.saveDraft(id: randomId, edit: true) + + self.completion(MediaEditorScreen.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -6204,11 +6501,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } }) }) - return } - if !self.isEditingStory { + if !(self.isEditingStory || self.isEditingStoryCover) { let privacy = self.state.privacy let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in if let current { @@ -6223,8 +6519,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.saveDraft(id: randomId) var firstFrame: Signal<(UIImage?, UIImage?), NoError> - let firstFrameTime = CMTime(seconds: mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60)) - + let firstFrameTime: CMTime + if let coverImageTimestamp = mediaEditor.values.coverImageTimestamp { + firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60)) + } else { + firstFrameTime = CMTime(seconds: mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60)) + } let videoResult: Signal var videoIsMirrored = false let duration: Double @@ -6466,7 +6766,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: inputImage, dimensions: storyDimensions, values: mediaEditor.values, time: firstFrameTime, textScale: 2.0, completion: { [weak self] coverImage in if let self { Logger.shared.log("MediaEditor", "Completed with video \(videoResult)") - self.completion(MediaEditorScreen.Result(media: .video(video: videoResult, coverImage: coverImage, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), mediaAreas: mediaAreas, caption: caption, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in + self.completion(MediaEditorScreen.Result(media: .video(video: videoResult, coverImage: coverImage, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -6489,7 +6789,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in if let self, let resultImage { Logger.shared.log("MediaEditor", "Completed with image \(resultImage)") - self.completion(MediaEditorScreen.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in + self.completion(MediaEditorScreen.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -6637,6 +6937,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate media: .sticker(file: file, emoji: self.effectiveStickerEmoji()), mediaAreas: [], caption: NSAttributedString(), + coverTimestamp: nil, options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false), stickers: [], randomId: 0 @@ -7375,12 +7676,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private final class DoneButtonContentComponent: CombinedComponent { let backgroundColor: UIColor - let icon: UIImage + let icon: UIImage? let title: String? init( backgroundColor: UIColor, - icon: UIImage, + icon: UIImage?, title: String? ) { self.backgroundColor = backgroundColor @@ -7404,12 +7705,14 @@ private final class DoneButtonContentComponent: CombinedComponent { let text = Child(Text.self) return { context in - let iconSize = context.component.icon.size - let icon = icon.update( - component: Image(image: context.component.icon, tintColor: .white, size: iconSize), - availableSize: CGSize(width: 180.0, height: 100.0), - transition: .immediate - ) + var iconChild: _UpdatedChildComponent? + if let iconImage = context.component.icon { + iconChild = icon.update( + component: Image(image: iconImage, tintColor: .white, size: iconImage.size), + availableSize: CGSize(width: 180.0, height: 100.0), + transition: .immediate + ) + } let backgroundHeight: CGFloat = 33.0 var backgroundSize = CGSize(width: backgroundHeight, height: backgroundHeight) @@ -7429,7 +7732,10 @@ private final class DoneButtonContentComponent: CombinedComponent { transition: .immediate ) - let updatedBackgroundWidth = backgroundSize.width + textSpacing + title!.size.width + var updatedBackgroundWidth = backgroundSize.width + title!.size.width + if let _ = iconChild { + updatedBackgroundWidth += textSpacing + } if updatedBackgroundWidth < 126.0 { backgroundSize.width = updatedBackgroundWidth } else { @@ -7449,16 +7755,22 @@ private final class DoneButtonContentComponent: CombinedComponent { ) if let title { + var titlePosition = backgroundSize.width / 2.0 + if let _ = iconChild { + titlePosition = title.size.width / 2.0 + 15.0 + } context.add(title - .position(CGPoint(x: title.size.width / 2.0 + 15.0, y: backgroundHeight / 2.0)) + .position(CGPoint(x: titlePosition, y: backgroundHeight / 2.0)) .opacity(hideTitle ? 0.0 : 1.0) ) } - context.add(icon - .position(CGPoint(x: background.size.width - 16.0, y: backgroundSize.height / 2.0)) - ) - + if let iconChild { + context.add(iconChild + .position(CGPoint(x: background.size.width - 16.0, y: backgroundSize.height / 2.0)) + ) + } + return backgroundSize } } @@ -7991,7 +8303,7 @@ private func stickerFile(resource: TelegramMediaResource, thumbnailResource: Tel fileAttributes.append(.FileName(fileName: isVideo ? "sticker.webm" : "sticker.webp")) fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) if isVideo { - fileAttributes.append(.Video(duration: duration ?? 3.0, size: dimensions, flags: [], preloadSize: nil)) + fileAttributes.append(.Video(duration: duration ?? 3.0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)) } else { fileAttributes.append(.ImageSize(size: dimensions)) } @@ -8002,3 +8314,27 @@ private func stickerFile(resource: TelegramMediaResource, thumbnailResource: Tel return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: isVideo ? "video/webm" : "image/webp", size: size, attributes: fileAttributes) } + +private struct MediaEditorConfiguration { + static var defaultValue: MediaEditorConfiguration { + return MediaEditorConfiguration(preloadWeather: true) + } + + let preloadWeather: Bool + + fileprivate init(preloadWeather: Bool) { + self.preloadWeather = preloadWeather + } + + static func with(appConfiguration: AppConfiguration) -> MediaEditorConfiguration { + if let data = appConfiguration.data { + var preloadWeather = false + if let value = data["story_weather_preload"] as? Bool { + preloadWeather = value + } + return MediaEditorConfiguration(preloadWeather: preloadWeather) + } else { + return .defaultValue + } + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift index c0f5a74ee81..bd968283849 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift @@ -4,14 +4,6 @@ import Display import CoreImage import MediaEditor -func createEmitterBehavior(type: String) -> NSObject { - let selector = ["behaviorWith", "Type:"].joined(separator: "") - let behaviorClass = NSClassFromString(["CA", "Emitter", "Behavior"].joined(separator: "")) as! NSObject.Type - let behaviorWithType = behaviorClass.method(for: NSSelectorFromString(selector))! - let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self) - return castedBehaviorWithType(behaviorClass, NSSelectorFromString(selector), type) -} - private var previousBeginTime: Int = 3 final class StickerCutoutOutlineView: UIView { @@ -81,7 +73,7 @@ final class StickerCutoutOutlineView: UIView { let lineEmitterCell = CAEmitterCell() lineEmitterCell.beginTime = CACurrentMediaTime() - let lineAlphaBehavior = createEmitterBehavior(type: "valueOverLife") + let lineAlphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") lineAlphaBehavior.setValue("color.alpha", forKey: "keyPath") lineAlphaBehavior.setValue([0.0, 0.5, 0.8, 0.5, 0.0], forKey: "values") lineEmitterCell.setValue([lineAlphaBehavior], forKey: "emitterBehaviors") @@ -107,7 +99,7 @@ final class StickerCutoutOutlineView: UIView { let glowEmitterCell = CAEmitterCell() glowEmitterCell.beginTime = CACurrentMediaTime() - let glowAlphaBehavior = createEmitterBehavior(type: "valueOverLife") + let glowAlphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") glowAlphaBehavior.setValue("color.alpha", forKey: "keyPath") glowAlphaBehavior.setValue([0.0, 0.32, 0.4, 0.2, 0.0], forKey: "values") glowEmitterCell.setValue([glowAlphaBehavior], forKey: "emitterBehaviors") diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift new file mode 100644 index 00000000000..bc9b2475cc6 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift @@ -0,0 +1,236 @@ +import Foundation +import CoreLocation +import SwiftSignalKit +import TelegramCore +import StickerPickerScreen +import AccountContext +import DeviceLocationManager +import DeviceAccess + +struct StoryWeather { + let emoji: String + let temperature: Double +} + +private func getWeatherData(context: AccountContext, location: CLLocationCoordinate2D) -> Signal { + let appConfiguration = context.currentAppConfiguration.with { $0 } + let botConfiguration = WeatherBotConfiguration.with(appConfiguration: appConfiguration) + + if let botUsername = botConfiguration.botName { + return context.engine.peers.resolvePeerByName(name: botUsername) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> mapToSignal { peer -> Signal in + guard let peer = peer else { + return .single(nil) + } + return context.engine.messages.requestChatContextResults(botId: peer.id, peerId: context.account.peerId, query: "", location: .single((location.latitude, location.longitude)), offset: "") + |> map { results -> ChatContextResultCollection? in + return results?.results + } + |> `catch` { error -> Signal in + return .single(nil) + } + } + |> map { contextResult -> StoryWeather? in + guard let contextResult, let result = contextResult.results.first, let emoji = result.title, let temperature = result.description.flatMap(Double.init) else { + return nil + } + return StoryWeather(emoji: emoji, temperature: temperature) + } + } else { + return .single(nil) + } +} + +func getWeather(context: AccountContext, load: Bool) -> Signal { + guard let locationManager = context.sharedContext.locationManager else { + return .single(.none) + } + + return DeviceAccess.authorizationStatus(subject: .location(.send)) + |> mapToSignal { status in + switch status { + case .notDetermined: + return .single(.notDetermined) + case .denied, .restricted, .unreachable: + return .single(.notAllowed) + case .allowed: + if load { + return .single(.fetching) + |> then( + currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0) + |> mapToSignal { location in + if let location { + return getWeatherData(context: context, location: location) + |> mapToSignal { weather in + if let weather { + let effectiveEmoji = emojiFor(for: weather.emoji.strippedEmoji, date: Date(), location: location) + if let match = context.animatedEmojiStickersValue[effectiveEmoji]?.first { + return .single(.loaded(StickerPickerScreen.Weather.LoadedWeather( + emoji: effectiveEmoji, + emojiFile: match.file, + temperature: weather.temperature + ))) + } else { + return .single(.none) + } + } else { + return .single(.none) + } + } + } else { + return .single(.none) + } + } + ) + } else { + return .single(.notPreloaded) + } + } + } +} + +private struct WeatherBotConfiguration { + static var defaultValue: WeatherBotConfiguration { + return WeatherBotConfiguration(botName: "izweatherbot") + } + + let botName: String? + + fileprivate init(botName: String?) { + self.botName = botName + } + + public static func with(appConfiguration: AppConfiguration) -> WeatherBotConfiguration { + if let data = appConfiguration.data, let botName = data["weather_search_username"] as? String { + return WeatherBotConfiguration(botName: botName) + } else { + return .defaultValue + } + } +} + +private let J1970: Double = 2440588.0 +private let moonEmojis = ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘", "🌑"] + +private func emojiFor(for emoji: String, date: Date, location: CLLocationCoordinate2D) -> String { + var emoji = emoji + if !"".isEmpty, ["☀️", "🌤️"].contains(emoji) && !isDay(latitude: location.latitude, longitude: location.longitude, dateTime: date) { + emoji = moonPhaseEmoji(for: date) + } + return emoji +} + +private func moonPhaseEmoji(for date: Date) -> String { + let julianDate = toJulianDate(date: date) + + let referenceNewMoon: Double = 2451550.1 + let synodicMonth: Double = 29.53058867 + + let daysSinceNewMoon = julianDate - referenceNewMoon + let newMoons = daysSinceNewMoon / synodicMonth + let currentMoonPhase = (newMoons - floor(newMoons)) * synodicMonth + + switch currentMoonPhase { + case 0..<1.84566: + return moonEmojis[0] + case 1.84566..<5.53699: + return moonEmojis[1] + case 5.53699..<9.22831: + return moonEmojis[2] + case 9.22831..<12.91963: + return moonEmojis[3] + case 12.91963..<16.61096: + return moonEmojis[4] + case 16.61096..<20.30228: + return moonEmojis[5] + case 20.30228..<23.99361: + return moonEmojis[6] + case 23.99361..<27.68493: + return moonEmojis[7] + default: + return moonEmojis[8] + } +} + +private func isDay(latitude: Double, longitude: Double, dateTime: Date) -> Bool { + let calendar = Calendar.current + let date = calendar.startOfDay(for: dateTime) + let time = dateTime.timeIntervalSince(date) + + let sunrise = calculateSunrise(latitude: latitude, longitude: longitude, date: date) + let sunset = calculateSunset(latitude: latitude, longitude: longitude, date: date) + + return time >= sunrise * 3600 && time <= sunset * 3600 +} + +private func calculateSunrise(latitude: Double, longitude: Double, date: Date) -> Double { + return calculateSunTime(latitude: latitude, longitude: longitude, date: date, isSunrise: true) +} + +private func calculateSunset(latitude: Double, longitude: Double, date: Date) -> Double { + return calculateSunTime(latitude: latitude, longitude: longitude, date: date, isSunrise: false) +} + +private func calculateSunTime(latitude: Double, longitude: Double, date: Date, isSunrise: Bool) -> Double { + let calendar = Calendar.current + let dayOfYear = calendar.ordinality(of: .day, in: .year, for: date)! + let zenith = 90.833 + + let D2R = Double.pi / 180.0 + let R2D = 180.0 / Double.pi + + let lngHour = longitude / 15.0 + let t = Double(dayOfYear) + ((isSunrise ? 6.0 : 18.0) - lngHour) / 24.0 + + let M = (0.9856 * t) - 3.289 + var L = M + (1.916 * sin(M * D2R)) + (0.020 * sin(2 * M * D2R)) + 282.634 + + if L > 360.0 { + L -= 360.0 + } else if L < 0.0 { + L += 360.0 + } + + var RA = R2D * atan(0.91764 * tan(L * D2R)) + if RA > 360.0 { + RA -= 360.0 + } else if RA < 0.0 { + RA += 360.0 + } + + let Lquadrant = (floor(L / 90.0)) * 90.0 + let RAquadrant = (floor(RA / 90.0)) * 90.0 + RA += (Lquadrant - RAquadrant) + RA /= 15.0 + + let sinDec = 0.39782 * sin(L * D2R) + let cosDec = cos(asin(sinDec)) + + let cosH = (cos(zenith * D2R) - (sinDec * sin(latitude * D2R))) / (cosDec * cos(latitude * D2R)) + if cosH > 1.0 || cosH < -1.0 { + return -1 + } + + var H = isSunrise ? (360.0 - R2D * acos(cosH)) : R2D * acos(cosH) + H /= 15.0 + + let T = H + RA - (0.06571 * t) - 6.622 + var UT = T - lngHour + + if UT > 24.0 { + UT -= 24.0 + } else if UT < 0.0 { + UT += 24.0 + } + return UT +} + +private func toJulianDate(date: Date) -> Double { + return date.timeIntervalSince1970 / 86400.0 + J1970 - 0.5 +} diff --git a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift index c08a62d34ec..7c955ae8a1f 100644 --- a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift @@ -70,6 +70,7 @@ public final class MediaScrubberComponent: Component { public enum Style { case editor case videoMessage + case cover } let context: AccountContext @@ -84,8 +85,10 @@ public final class MediaScrubberComponent: Component { let isPlaying: Bool let tracks: [Track] + let portalView: PortalView? let positionUpdated: (Double, Bool) -> Void + let coverPositionUpdated: (Double, Bool, @escaping () -> Void) -> Void let trackTrimUpdated: (Int32, Double, Double, Bool, Bool) -> Void let trackOffsetUpdated: (Int32, Double, Bool) -> Void let trackLongPressed: (Int32, UIView) -> Void @@ -100,7 +103,9 @@ public final class MediaScrubberComponent: Component { maxDuration: Double, isPlaying: Bool, tracks: [Track], + portalView: PortalView? = nil, positionUpdated: @escaping (Double, Bool) -> Void, + coverPositionUpdated: @escaping (Double, Bool, @escaping () -> Void) -> Void = { _, _, _ in }, trackTrimUpdated: @escaping (Int32, Double, Double, Bool, Bool) -> Void, trackOffsetUpdated: @escaping (Int32, Double, Bool) -> Void, trackLongPressed: @escaping (Int32, UIView) -> Void @@ -114,7 +119,9 @@ public final class MediaScrubberComponent: Component { self.maxDuration = maxDuration self.isPlaying = isPlaying self.tracks = tracks + self.portalView = portalView self.positionUpdated = positionUpdated + self.coverPositionUpdated = coverPositionUpdated self.trackTrimUpdated = trackTrimUpdated self.trackOffsetUpdated = trackOffsetUpdated self.trackLongPressed = trackLongPressed @@ -152,6 +159,7 @@ public final class MediaScrubberComponent: Component { private var trackViews: [Int32: TrackView] = [:] private let trimView: TrimView private let ghostTrimView: TrimView + private let cursorContentView: UIView private let cursorView: HandleView private var cursorDisplayLink: SharedDisplayLinkDriver.Link? @@ -159,6 +167,7 @@ public final class MediaScrubberComponent: Component { private var selectedTrackId: Int32 = 0 private var isPanningCursor = false + private var ignoreCursorPositionUpdate = false private var scrubberSize: CGSize? @@ -169,6 +178,7 @@ public final class MediaScrubberComponent: Component { self.trimView = TrimView(frame: .zero) self.ghostTrimView = TrimView(frame: .zero) self.ghostTrimView.isHollow = true + self.cursorContentView = UIView() self.cursorView = HandleView() super.init(frame: frame) @@ -178,6 +188,10 @@ public final class MediaScrubberComponent: Component { self.disablesInteractiveModalDismiss = true self.disablesInteractiveKeyboardGestureRecognizer = true + self.cursorContentView.isUserInteractionEnabled = false + self.cursorContentView.clipsToBounds = true + self.cursorContentView.layer.cornerRadius = 10.0 + let positionImage = generateImage(CGSize(width: handleWidth, height: 50.0), rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size)) context.setFillColor(UIColor.white.cgColor) @@ -187,13 +201,13 @@ public final class MediaScrubberComponent: Component { context.addPath(path.cgPath) context.fillPath() })?.stretchableImage(withLeftCapWidth: Int(handleWidth / 2.0), topCapHeight: 25) - self.cursorView.image = positionImage self.cursorView.isUserInteractionEnabled = true self.cursorView.hitTestSlop = UIEdgeInsets(top: -8.0, left: -9.0, bottom: -8.0, right: -9.0) self.addSubview(self.ghostTrimView) self.addSubview(self.trimView) + self.addSubview(self.cursorContentView) self.addSubview(self.cursorView) self.cursorView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handleCursorPan(_:)))) @@ -317,10 +331,18 @@ public final class MediaScrubberComponent: Component { switch gestureRecognizer.state { case .began, .changed: self.isPanningCursor = true - component.positionUpdated(position, false) + if case .cover = component.style { + component.coverPositionUpdated(position, false, {}) + } else { + component.positionUpdated(position, false) + } case .ended, .cancelled: self.isPanningCursor = false - component.positionUpdated(position, true) + if case .cover = component.style { + component.coverPositionUpdated(position, false, {}) + } else { + component.positionUpdated(position, true) + } default: break } @@ -328,10 +350,23 @@ public final class MediaScrubberComponent: Component { } private func cursorFrame(size: CGSize, height: CGFloat, position: Double, duration : Double) -> CGRect { + var cursorWidth = handleWidth + var cursorMargin = handleWidth + var height = height + var isCover = false + var y: CGFloat = -5.0 - UIScreenPixel + if let component = self.component, case .cover = component.style { + cursorWidth = 30.0 + 12.0 + cursorMargin = handleWidth + height = 50.0 + isCover = true + y += 1.0 + } + let cursorPadding: CGFloat = 8.0 let cursorPositionFraction = duration > 0.0 ? position / duration : 0.0 - let cursorPosition = floorToScreenPixels(handleWidth - 1.0 + (size.width - handleWidth * 2.0 + 2.0) * cursorPositionFraction) - var cursorFrame = CGRect(origin: CGPoint(x: cursorPosition - handleWidth / 2.0, y: -5.0 - UIScreenPixel), size: CGSize(width: handleWidth, height: height)) + let cursorPosition = floorToScreenPixels(cursorMargin - 1.0 + (size.width - handleWidth * 2.0 + 2.0) * cursorPositionFraction) + var cursorFrame = CGRect(origin: CGPoint(x: cursorPosition - cursorWidth / 2.0, y: y), size: CGSize(width: cursorWidth, height: height)) var leftEdge = self.ghostTrimView.leftHandleView.frame.maxX var rightEdge = self.ghostTrimView.rightHandleView.frame.minX @@ -339,9 +374,13 @@ public final class MediaScrubberComponent: Component { leftEdge = self.trimView.leftHandleView.frame.maxX rightEdge = self.trimView.rightHandleView.frame.minX } + if isCover { + leftEdge = 0.0 + rightEdge = size.width + } cursorFrame.origin.x = max(leftEdge - cursorPadding, cursorFrame.origin.x) - cursorFrame.origin.x = min(rightEdge - handleWidth + cursorPadding, cursorFrame.origin.x) + cursorFrame.origin.x = min(rightEdge - cursorWidth + cursorPadding, cursorFrame.origin.x) return cursorFrame } @@ -377,6 +416,7 @@ public final class MediaScrubberComponent: Component { updatedPosition = max(self.startPosition, min(self.endPosition, position + advance)) } self.cursorView.frame = cursorFrame(size: scrubberSize, height: self.effectiveCursorHeight, position: updatedPosition, duration: self.trimDuration) + self.cursorContentView.frame = self.cursorView.frame.insetBy(dx: 6.0, dy: 2.0).offsetBy(dx: -1.0 - UIScreenPixel, dy: 0.0) } public func update(component: MediaScrubberComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -384,11 +424,36 @@ public final class MediaScrubberComponent: Component { self.component = component self.state = state + if let portalView = component.portalView, portalView.view.superview == nil { + portalView.view.frame = CGRect(x: 0.0, y: 0.0, width: 30.0, height: 48.0) + portalView.view.clipsToBounds = true + self.cursorContentView.addSubview(portalView.view) + } + switch component.style { case .editor: self.cursorView.isHidden = false case .videoMessage: self.cursorView.isHidden = true + case .cover: + self.cursorView.isHidden = false + self.trimView.isHidden = true + self.ghostTrimView.isHidden = true + + if isFirstTime { + let positionImage = generateImage(CGSize(width: 30.0 + 12.0, height: 50.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setStrokeColor(UIColor.white.cgColor) + let lineWidth = 2.0 - UIScreenPixel + context.setLineWidth(lineWidth) + context.setShadow(offset: .zero, blur: 2.0, color: UIColor(rgb: 0x000000, alpha: 0.55).cgColor) + + let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 6.0 - lineWidth / 2.0, y: 2.0 - lineWidth / 2.0), size: CGSize(width: 30.0 - lineWidth, height: 48.0 - lineWidth)), cornerRadius: 9.0) + context.addPath(path.cgPath) + context.strokePath() + }) + self.cursorView.image = positionImage + } } var totalHeight: CGFloat = 0.0 @@ -419,6 +484,23 @@ public final class MediaScrubberComponent: Component { } else { trackTransition = .immediate trackView = TrackView() + trackView.onTap = { [weak self] fraction in + guard let self, let component = self.component else { + return + } + var position = max(self.startPosition, min(self.endPosition, self.trimDuration * fraction)) + if let offset = self.mainAudioTrackOffset { + position += offset + } + self.ignoreCursorPositionUpdate = true + component.coverPositionUpdated(position, true, { [weak self] in + guard let self else { + return + } + self.ignoreCursorPositionUpdate = false + self.state?.updated(transition: .immediate) + }) + } trackView.onSelection = { [weak self] id in guard let self else { return @@ -520,7 +602,7 @@ public final class MediaScrubberComponent: Component { let fullTrackHeight: CGFloat switch component.style { - case .editor: + case .editor, .cover: fullTrackHeight = trackHeight case .videoMessage: fullTrackHeight = 33.0 @@ -583,7 +665,7 @@ public final class MediaScrubberComponent: Component { transition.setFrame(view: self.ghostTrimView, frame: ghostTrimViewFrame) transition.setAlpha(view: self.ghostTrimView, alpha: ghostTrimVisible ? 0.75 : 0.0) - if case .videoMessage = component.style { + if [.videoMessage, .cover].contains(component.style) { for (_ , trackView) in self.trackViews { trackView.updateOpaqueEdges( left: leftHandleFrame.minX, @@ -606,11 +688,15 @@ public final class MediaScrubberComponent: Component { self.cursorPositionAnimation = nil self.cursorDisplayLink?.isPaused = true - var cursorPosition = component.position - if let offset = self.mainAudioTrackOffset { - cursorPosition -= offset + if !self.ignoreCursorPositionUpdate { + var cursorPosition = component.position + if let offset = self.mainAudioTrackOffset { + cursorPosition -= offset + } + let cursorFrame = cursorFrame(size: scrubberSize, height: self.effectiveCursorHeight, position: cursorPosition, duration: trimDuration) + transition.setFrame(view: self.cursorView, frame: cursorFrame) + transition.setFrame(view: self.cursorContentView, frame: cursorFrame.insetBy(dx: 6.0, dy: 2.0).offsetBy(dx: -1.0 - UIScreenPixel, dy: 0.0)) } - transition.setFrame(view: self.cursorView, frame: cursorFrame(size: scrubberSize, height: self.effectiveCursorHeight, position: cursorPosition, duration: trimDuration)) } else { if let (_, _, end, ended) = self.cursorPositionAnimation { if ended, component.position >= self.startPosition && component.position < end - 1.0 { @@ -663,6 +749,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega fileprivate var videoOpaqueFrameLayers: [VideoFrameLayer] = [] var onSelection: (Int32) -> Void = { _ in } + var onTap: (CGFloat) -> Void = { _ in } var offsetUpdated: (Double, Bool) -> Void = { _, _ in } var updated: (ComponentTransition) -> Void = { _ in } @@ -716,7 +803,6 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega self.scrollView.delegate = self - self.videoTransparentFramesContainer.alpha = 0.5 self.videoTransparentFramesContainer.clipsToBounds = true self.videoTransparentFramesContainer.isUserInteractionEnabled = false @@ -739,10 +825,15 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega } @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { - guard let (track, _, _, _) = self.params else { + guard let params = self.params else { return } - self.onSelection(track.id) + if case .cover = params.style { + let location = gestureRecognizer.location(in: self) + self.onTap(location.x / self.frame.width) + } else { + self.onSelection(params.track.id) + } } private func updateTrackOffset(done: Bool) { @@ -786,6 +877,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega } private var params: ( + style: MediaScrubberComponent.Style, track: MediaScrubberComponent.Track, isSelected: Bool, availableSize: CGSize, @@ -834,21 +926,24 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega transition: ComponentTransition ) -> CGSize { let previousParams = self.params - self.params = (track, isSelected, availableSize, duration) + self.params = (style, track, isSelected, availableSize, duration) let fullTrackHeight: CGFloat let framesCornerRadius: CGFloat switch style { - case .editor: + case .editor, .cover: fullTrackHeight = trackHeight framesCornerRadius = 9.0 + self.videoTransparentFramesContainer.alpha = 0.35 case .videoMessage: fullTrackHeight = 33.0 framesCornerRadius = fullTrackHeight / 2.0 + self.videoTransparentFramesContainer.alpha = 0.5 } self.videoTransparentFramesContainer.layer.cornerRadius = framesCornerRadius self.videoOpaqueFramesContainer.layer.cornerRadius = framesCornerRadius + let scrubberSize = CGSize(width: availableSize.width, height: isSelected ? fullTrackHeight : collapsedTrackHeight) var screenSpanDuration = duration @@ -1362,7 +1457,7 @@ private class TrimView: UIView { let highlightColor: UIColor switch style { - case .editor: + case .editor, .cover: effectiveHandleWidth = handleWidth fullTrackHeight = trackHeight capsuleOffset = 5.0 - UIScreenPixel diff --git a/submodules/TelegramUI/Components/MiniAppListScreen/BUILD b/submodules/TelegramUI/Components/MiniAppListScreen/BUILD new file mode 100644 index 00000000000..02745a260c5 --- /dev/null +++ b/submodules/TelegramUI/Components/MiniAppListScreen/BUILD @@ -0,0 +1,39 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "MiniAppListScreen", + module_name = "MiniAppListScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/PresentationDataUtils", + "//submodules/AccountContext", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/MergeLists", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/ItemListUI", + "//submodules/ChatListUI", + "//submodules/ItemListPeerItem", + "//submodules/TelegramUI/Components/ChatListHeaderComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/SearchBarNode", + "//submodules/Components/BalancedTextComponent", + "//submodules/ChatListSearchItemHeader", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift new file mode 100644 index 00000000000..579bd7b8d98 --- /dev/null +++ b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift @@ -0,0 +1,809 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AccountContext +import ComponentFlow +import ViewControllerComponent +import MergeLists +import ComponentDisplayAdapters +import ItemListPeerItem +import ItemListUI +import ChatListHeaderComponent +import PlainButtonComponent +import MultilineTextComponent +import SearchBarNode +import BalancedTextComponent +import ChatListSearchItemHeader + +final class MiniAppListScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialData: MiniAppListScreen.InitialData + + init( + context: AccountContext, + initialData: MiniAppListScreen.InitialData + ) { + self.context = context + self.initialData = initialData + } + + static func ==(lhs: MiniAppListScreenComponent, rhs: MiniAppListScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + + return true + } + + private enum ContentEntry: Comparable, Identifiable { + enum Id: Hashable { + case item(EnginePeer.Id) + } + + var stableId: Id { + switch self { + case let .item(peer, _): + return .item(peer.id) + } + } + + case item(peer: EnginePeer, sortIndex: Int) + + static func <(lhs: ContentEntry, rhs: ContentEntry) -> Bool { + switch lhs { + case let .item(lhsPeer, lhsSortIndex): + switch rhs { + case let .item(rhsPeer, rhsSortIndex): + if lhsSortIndex != rhsSortIndex { + return lhsSortIndex < rhsSortIndex + } + return lhsPeer.id < rhsPeer.id + } + } + } + + func item(listNode: ContentListNode) -> ListViewItem { + switch self { + case let .item(peer, _): + let text: ItemListPeerItemText + if case let .user(user) = peer, let subscriberCount = user.subscriberCount { + text = .text(listNode.presentationData.strings.Conversation_StatusBotSubscribers(subscriberCount), .secondary) + } else { + text = .none + } + + return ItemListPeerItem( + presentationData: ItemListPresentationData(listNode.presentationData), + dateTimeFormat: listNode.presentationData.dateTimeFormat, + nameDisplayOrder: listNode.presentationData.nameDisplayOrder, + context: listNode.context, + peer: peer, + presence: nil, + text: text, + label: .none, + editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), + enabled: true, + selectable: true, + sectionId: 0, + action: { [weak listNode] in + guard let listNode else { + return + } + if let view = listNode.parentView { + view.openItem(peer: peer) + } + }, + setPeerIdWithRevealedOptions: { _, _ in + }, + removePeer: { _ in + }, + noInsets: true, + header: nil + ) + } + } + } + + private final class ContentListNode: ListView { + weak var parentView: View? + let context: AccountContext + var presentationData: PresentationData + private var currentEntries: [ContentEntry] = [] + private var originalEntries: [ContentEntry] = [] + + init(parentView: View, context: AccountContext) { + self.parentView = parentView + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + + super.init() + } + + func update(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) { + let (listViewDuration, listViewCurve) = listViewAnimationDurationAndCurve(transition: transition.containedViewLayoutTransition) + self.transaction( + deleteIndices: [], + insertIndicesAndItems: [], + updateIndicesAndItems: [], + options: [.Synchronous, .LowLatency, .PreferSynchronousResourceLoading], + additionalScrollDistance: 0.0, + updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: listViewDuration, curve: listViewCurve), + updateOpaqueState: nil + ) + } + + func setEntries(entries: [ContentEntry], animated: Bool) { + self.originalEntries = entries + + let entries = entries + + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.currentEntries, rightList: entries) + self.currentEntries = entries + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(listNode: self), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(listNode: self), directionHint: nil) } + + var options: ListViewDeleteAndInsertOptions = [.Synchronous, .LowLatency] + if animated { + options.insert(.AnimateInsertion) + } else { + options.insert(.PreferSynchronousResourceLoading) + } + + self.transaction( + deleteIndices: deletions, + insertIndicesAndItems: insertions, + updateIndicesAndItems: updates, + options: options, + scrollToItem: nil, + stationaryItemRange: nil, + updateOpaqueState: nil, + completion: { _ in + } + ) + } + } + + final class View: UIView { + private var contentListNode: ContentListNode? + private var ignoreVisibleContentOffsetChanged: Bool = false + private var emptySearchState: ComponentView? + + private let navigationBarView = ComponentView() + private var navigationHeight: CGFloat? + + private let sectionHeader = ComponentView() + + private var searchBarNode: SearchBarNode? + + private var isUpdating: Bool = false + + private var component: MiniAppListScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private var recommendedAppPeers: [EnginePeer]? + private var recommendedAppPeersDisposable: Disposable? + private var keepUpdatedDisposable: Disposable? + + private var isSearchDisplayControllerActive: Bool = false + private var searchQuery: String = "" + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.recommendedAppPeersDisposable?.dispose() + self.keepUpdatedDisposable?.dispose() + } + + func scrollToTop() { + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + return true + } + + func openItem(peer: EnginePeer) { + guard let component = self.component else { + return + } + guard let environment = self.environment, let controller = environment.controller() else { + return + } + + if let peerInfoScreen = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + peerInfoScreen.navigationPresentation = .modal + controller.push(peerInfoScreen) + } + } + + private func updateNavigationBar( + component: MiniAppListScreenComponent, + theme: PresentationTheme, + strings: PresentationStrings, + size: CGSize, + insets: UIEdgeInsets, + statusBarHeight: CGFloat, + isModal: Bool, + transition: ComponentTransition, + deferScrollApplication: Bool + ) -> CGFloat { + let rightButtons: [AnyComponentWithIdentity] = [] + + let titleText: String = strings.MiniAppList_Title + + let closeTitle: String = strings.Common_Close + let headerContent: ChatListHeaderComponent.Content? = ChatListHeaderComponent.Content( + title: titleText, + navigationBackTitle: nil, + titleComponent: nil, + chatListTitle: nil, + leftButton: isModal ? AnyComponentWithIdentity(id: "close", component: AnyComponent(NavigationButtonComponent( + content: .text(title: closeTitle, isBold: false), + pressed: { [weak self] _ in + guard let self else { + return + } + if self.attemptNavigation(complete: {}) { + self.environment?.controller()?.dismiss() + } + } + ))) : nil, + rightButtons: rightButtons, + backTitle: isModal ? nil : strings.Common_Back, + backPressed: { [weak self] in + guard let self else { + return + } + + if self.attemptNavigation(complete: {}) { + self.environment?.controller()?.dismiss() + } + } + ) + + let navigationBarSize = self.navigationBarView.update( + transition: transition, + component: AnyComponent(ChatListNavigationBar( + context: component.context, + theme: theme, + strings: strings, + statusBarHeight: statusBarHeight, + sideInset: insets.left, + isSearchActive: self.isSearchDisplayControllerActive, + isSearchEnabled: true, + primaryContent: headerContent, + secondaryContent: nil, + secondaryTransition: 0.0, + storySubscriptions: nil, + storiesIncludeHidden: false, + uploadProgress: [:], + tabsNode: nil, + tabsNodeIsSearch: false, + accessoryPanelContainer: nil, + accessoryPanelContainerHeight: 0.0, + activateSearch: { [weak self] _ in + guard let self else { + return + } + + self.isSearchDisplayControllerActive = true + self.state?.updated(transition: .spring(duration: 0.4)) + }, + openStatusSetup: { _ in + }, + allowAutomaticOrder: { + } + )), + environment: {}, + containerSize: size + ) + + let sectionHeaderSize = self.sectionHeader.update( + transition: transition, + component: AnyComponent(ListHeaderComponent( + theme: theme, + title: strings.MiniAppList_ListSectionHeader + )), + environment: {}, + containerSize: CGSize(width: size.width, height: 1000.0) + ) + if let sectionHeaderView = self.sectionHeader.view { + if sectionHeaderView.superview == nil { + sectionHeaderView.layer.anchorPoint = CGPoint() + self.addSubview(sectionHeaderView) + } + transition.setBounds(view: sectionHeaderView, bounds: CGRect(origin: CGPoint(), size: sectionHeaderSize)) + } + + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + if deferScrollApplication { + navigationBarComponentView.deferScrollApplication = true + } + + if navigationBarComponentView.superview == nil { + self.addSubview(navigationBarComponentView) + } + transition.setFrame(view: navigationBarComponentView, frame: CGRect(origin: CGPoint(), size: navigationBarSize)) + + return navigationBarSize.height + } else { + return 0.0 + } + } + + private func updateNavigationScrolling(navigationHeight: CGFloat, transition: ComponentTransition) { + var mainOffset: CGFloat + if let recommendedAppPeers = self.recommendedAppPeers, !recommendedAppPeers.isEmpty { + if let contentListNode = self.contentListNode { + switch contentListNode.visibleContentOffset() { + case .none: + mainOffset = 0.0 + case .unknown: + mainOffset = navigationHeight + case let .known(value): + mainOffset = value + } + } else { + mainOffset = navigationHeight + } + } else { + mainOffset = navigationHeight + } + + mainOffset = min(mainOffset, ChatListNavigationBar.searchScrollHeight) + if abs(mainOffset) < 0.1 { + mainOffset = 0.0 + } + + let resultingOffset = mainOffset + + var offset = resultingOffset + if self.isSearchDisplayControllerActive { + offset = 0.0 + } + + if let sectionHeaderView = self.sectionHeader.view { + transition.setPosition(view: sectionHeaderView, position: CGPoint(x: 0.0, y: navigationHeight - offset)) + } + + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: false, forceUpdate: false, transition: transition.withUserData(ChatListNavigationBar.AnimationHint( + disableStoriesAnimations: false, + crossfadeStoryPeers: false + ))) + } + } + + func update(component: MiniAppListScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + self.recommendedAppPeers = component.initialData.recommendedAppPeers + + /*self.shortcutMessageListDisposable = (component.context.engine.accountData.shortcutMessageList(onlyRemote: false) + |> deliverOnMainQueue).startStrict(next: { [weak self] shortcutMessageList in + guard let self else { + return + } + self.shortcutMessageList = shortcutMessageList + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + })*/ + + self.keepUpdatedDisposable = component.context.engine.peers.requestRecommendedAppsIfNeeded().startStrict() + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + self.component = component + self.state = state + + if themeUpdated { + self.backgroundColor = environment.theme.list.plainBackgroundColor + } + + var isModal = false + if let controller = environment.controller(), controller.navigationPresentation == .modal { + isModal = true + } + + var statusBarHeight = environment.statusBarHeight + if isModal { + statusBarHeight = max(statusBarHeight, 1.0) + } + + let listBottomInset = environment.safeInsets.bottom + environment.additionalInsets.bottom + let navigationHeight = self.updateNavigationBar( + component: component, + theme: environment.theme, + strings: environment.strings, + size: availableSize, + insets: environment.safeInsets, + statusBarHeight: statusBarHeight, + isModal: isModal, + transition: transition, + deferScrollApplication: true + ) + self.navigationHeight = navigationHeight + + var removedSearchBar: SearchBarNode? + if self.isSearchDisplayControllerActive { + let searchBarNode: SearchBarNode + var searchBarTransition = transition + if let current = self.searchBarNode { + searchBarNode = current + } else { + searchBarTransition = .immediate + let searchBarTheme = SearchBarNodeTheme(theme: environment.theme, hasSeparator: false) + searchBarNode = SearchBarNode( + theme: searchBarTheme, + strings: environment.strings, + fieldStyle: .modern, + displayBackground: false + ) + searchBarNode.placeholderString = NSAttributedString(string: environment.strings.Common_Search, font: Font.regular(17.0), textColor: searchBarTheme.placeholder) + self.searchBarNode = searchBarNode + searchBarNode.cancel = { [weak self] in + guard let self else { + return + } + self.isSearchDisplayControllerActive = false + self.state?.updated(transition: .spring(duration: 0.4)) + } + searchBarNode.textUpdated = { [weak self] query, _ in + guard let self else { + return + } + if self.searchQuery != query { + self.searchQuery = query.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + self.state?.updated(transition: .immediate) + } + } + DispatchQueue.main.async { [weak self, weak searchBarNode] in + guard let self, let searchBarNode, self.searchBarNode === searchBarNode else { + return + } + searchBarNode.activate() + } + } + + var searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight - 54.0 + 2.0), size: CGSize(width: availableSize.width, height: 54.0)) + if isModal { + searchBarFrame.origin.y += 2.0 + } + searchBarNode.updateLayout(boundingSize: searchBarFrame.size, leftInset: environment.safeInsets.left + 6.0, rightInset: environment.safeInsets.right, transition: searchBarTransition.containedViewLayoutTransition) + searchBarTransition.setFrame(view: searchBarNode.view, frame: searchBarFrame) + if searchBarNode.view.superview == nil { + self.addSubview(searchBarNode.view) + + if case let .curve(duration, curve) = transition.animation, let navigationBarView = self.navigationBarView.view as? ChatListNavigationBar.View, let placeholderNode = navigationBarView.searchContentNode?.placeholderNode { + let timingFunction: String + switch curve { + case .easeInOut: + timingFunction = CAMediaTimingFunctionName.easeOut.rawValue + case .linear: + timingFunction = CAMediaTimingFunctionName.linear.rawValue + case .spring: + timingFunction = kCAMediaTimingFunctionSpring + case .custom: + timingFunction = kCAMediaTimingFunctionSpring + } + + searchBarNode.animateIn(from: placeholderNode, duration: duration, timingFunction: timingFunction) + } + } + } else { + self.searchQuery = "" + if let searchBarNode = self.searchBarNode { + self.searchBarNode = nil + removedSearchBar = searchBarNode + } + } + + let contentListNode: ContentListNode + if let current = self.contentListNode { + contentListNode = current + } else { + contentListNode = ContentListNode(parentView: self, context: component.context) + self.contentListNode = contentListNode + + contentListNode.visibleContentOffsetChanged = { [weak self] offset in + guard let self else { + return + } + guard let navigationHeight = self.navigationHeight else { + return + } + if self.ignoreVisibleContentOffsetChanged { + return + } + self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: .immediate) + } + + if let sectionHeaderView = self.sectionHeader.view { + self.insertSubview(contentListNode.view, belowSubview: sectionHeaderView) + } else if let navigationBarComponentView = self.navigationBarView.view { + self.insertSubview(contentListNode.view, belowSubview: navigationBarComponentView) + } else { + self.addSubview(contentListNode.view) + } + } + + var contentTopInset = navigationHeight + if let sectionHeaderView = self.sectionHeader.view { + contentTopInset += sectionHeaderView.bounds.height + } + transition.setFrame(view: contentListNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) + self.ignoreVisibleContentOffsetChanged = true + contentListNode.update(size: availableSize, insets: UIEdgeInsets(top: contentTopInset, left: environment.safeInsets.left, bottom: listBottomInset, right: environment.safeInsets.right), transition: transition) + self.ignoreVisibleContentOffsetChanged = false + + var entries: [ContentEntry] = [] + if let recommendedAppPeers = self.recommendedAppPeers { + let normalizedSearchQuery = self.searchQuery.lowercased().trimmingTrailingSpaces() + for peer in recommendedAppPeers { + if !self.searchQuery.isEmpty { + var matches = false + if peer.indexName.matchesByTokens(normalizedSearchQuery) { + matches = true + } + if !matches { + continue + } + } + entries.append(.item(peer: peer, sortIndex: entries.count)) + } + } + contentListNode.setEntries(entries: entries, animated: !transition.animation.isImmediate) + if let sectionHeaderView = self.sectionHeader.view { + sectionHeaderView.isHidden = entries.isEmpty + } + + if !self.searchQuery.isEmpty && entries.isEmpty { + var emptySearchStateTransition = transition + let emptySearchState: ComponentView + if let current = self.emptySearchState { + emptySearchState = current + } else { + emptySearchStateTransition = emptySearchStateTransition.withAnimation(.none) + emptySearchState = ComponentView() + self.emptySearchState = emptySearchState + } + let emptySearchStateSize = emptySearchState.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(NSAttributedString(string: environment.strings.Conversation_SearchNoResults, font: Font.regular(17.0), textColor: environment.theme.list.freeTextColor, paragraphAlignment: .center)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: availableSize.height) + ) + var emptySearchStateBottomInset = listBottomInset + emptySearchStateBottomInset = max(emptySearchStateBottomInset, environment.inputHeight) + let emptySearchStateFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - emptySearchStateSize.width) * 0.5), y: navigationHeight + floor((availableSize.height - emptySearchStateBottomInset - navigationHeight) * 0.5)), size: emptySearchStateSize) + if let emptySearchStateView = emptySearchState.view { + if emptySearchStateView.superview == nil { + if let navigationBarComponentView = self.navigationBarView.view { + self.insertSubview(emptySearchStateView, belowSubview: navigationBarComponentView) + } else { + self.addSubview(emptySearchStateView) + } + } + emptySearchStateTransition.containedViewLayoutTransition.updatePosition(layer: emptySearchStateView.layer, position: emptySearchStateFrame.center) + emptySearchStateView.bounds = CGRect(origin: CGPoint(), size: emptySearchStateFrame.size) + } + } else if let emptySearchState = self.emptySearchState { + self.emptySearchState = nil + emptySearchState.view?.removeFromSuperview() + } + + if let recommendedAppPeers = self.recommendedAppPeers, !recommendedAppPeers.isEmpty { + contentListNode.isHidden = false + } else { + contentListNode.isHidden = true + } + + self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: transition) + + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + navigationBarComponentView.deferScrollApplication = false + navigationBarComponentView.applyCurrentScroll(transition: transition) + } + + if let removedSearchBar { + if !transition.animation.isImmediate, let navigationBarView = self.navigationBarView.view as? ChatListNavigationBar.View, let placeholderNode = + navigationBarView.searchContentNode?.placeholderNode { + removedSearchBar.transitionOut(to: placeholderNode, transition: transition.containedViewLayoutTransition, completion: { [weak removedSearchBar] in + removedSearchBar?.view.removeFromSuperview() + }) + } else { + removedSearchBar.view.removeFromSuperview() + } + } + + return availableSize + } + } + + 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, state: state, environment: environment, transition: transition) + } +} + +public final class MiniAppListScreen: ViewControllerComponentContainer { + public final class InitialData: MiniAppListScreenInitialData { + let recommendedAppPeers: [EnginePeer] + + init( + recommendedAppPeers: [EnginePeer] + ) { + self.recommendedAppPeers = recommendedAppPeers + } + } + + private let context: AccountContext + + public init(context: AccountContext, initialData: InitialData) { + self.context = context + + super.init(context: context, component: MiniAppListScreenComponent( + context: context, + initialData: initialData + ), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil) + + self.navigationPresentation = .modal + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? MiniAppListScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? MiniAppListScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + + public static func initialData(context: AccountContext) -> Signal { + let recommendedAppPeers = context.engine.peers.recommendedAppPeerIds() + |> take(1) + |> mapToSignal { peerIds -> Signal<[EnginePeer], NoError> in + guard let peerIds else { + return .single([]) + } + return context.engine.data.get( + EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + |> map { peers -> [EnginePeer] in + return peers.compactMap { $0 } + } + } + + return recommendedAppPeers + |> map { recommendedAppPeers -> MiniAppListScreenInitialData in + return InitialData( + recommendedAppPeers: recommendedAppPeers + ) + } + } +} + +private final class ListHeaderComponent: Component { + let theme: PresentationTheme + let title: String + + init( + theme: PresentationTheme, + title: String + ) { + self.theme = theme + self.title = title + } + + static func ==(lhs: ListHeaderComponent, rhs: ListHeaderComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + final class View: UIView { + private let title = ComponentView() + + private var component: ListHeaderComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if self.component?.theme !== component.theme { + self.backgroundColor = component.theme.chatList.sectionHeaderFillColor + } + + let insets = UIEdgeInsets(top: 7.0, left: 16.0, bottom: 7.0, right: 16.0) + + let titleString = component.title + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleString, font: Font.regular(13.0), textColor: component.theme.chatList.sectionHeaderTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - insets.left - insets.right, height: 100.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: titleSize) + } + + return CGSize(width: availableSize.width, height: titleSize.height + insets.top + insets.bottom) + } + } + + 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) + } +} diff --git a/submodules/TelegramUI/Components/MinimizedContainer/BUILD b/submodules/TelegramUI/Components/MinimizedContainer/BUILD index 5f2bbce4969..514280d85b5 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/BUILD +++ b/submodules/TelegramUI/Components/MinimizedContainer/BUILD @@ -14,9 +14,12 @@ swift_library( "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/TelegramPresentationData", - "//submodules/ComponentFlow", "//submodules/AccountContext", "//submodules/UIKitRuntimeUtils", + "//submodules/ComponentFlow", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift index 7301427ffe4..b316bf2f11a 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift @@ -10,13 +10,14 @@ import UIKitRuntimeUtils private let minimizedNavigationHeight: CGFloat = 44.0 private let minimizedTopMargin: CGFloat = 3.0 +private let maximizeLastStandingController = false final class ScrollViewImpl: UIScrollView { - var passthrough = false + var shouldPassthrough: () -> Bool = { return false } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) - if result === self && self.passthrough { + if result === self && self.shouldPassthrough() { return nil } return result @@ -26,13 +27,27 @@ final class ScrollViewImpl: UIScrollView { public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScrollViewDelegate, ASGestureRecognizerDelegate { final class Item { let id: AnyHashable - let controller: ViewController + let controller: MinimizableController let beforeMaximize: (NavigationController, @escaping () -> Void) -> Void + let topEdgeOffset: CGFloat? - init(id: AnyHashable, controller: ViewController, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void) { + init( + id: AnyHashable, + controller: MinimizableController, + beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void, + topEdgeOffset: CGFloat? + ) { self.id = id self.controller = controller self.beforeMaximize = beforeMaximize + self.topEdgeOffset = topEdgeOffset + } + } + + final class SnapshotContainerView: UIView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + self.removeFromSuperview() + return nil } } @@ -54,7 +69,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll private let shadowNode: ASImageNode private var controllerView: UIView? - fileprivate let snapshotContainerView = UIView() + fileprivate let snapshotContainerView = SnapshotContainerView() fileprivate var snapshotView: UIView? fileprivate var blurredSnapshotView: UIView? @@ -98,7 +113,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll self.snapshotContainerView.isUserInteractionEnabled = false super.init() - + self.clipsToBounds = true self.cornerRadius = 10.0 applySmoothRoundedCorners(self.layer) @@ -112,7 +127,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll Queue.mainQueue().after(0.45) { self.isReady = true - if !self.isDismissed, let snapshotView = self.controllerView?.snapshotView(afterScreenUpdates: false) { + if !self.isDismissed, let snapshotView = self.item.controller.makeContentSnapshotView() { self.containerNode.view.addSubview(self.snapshotContainerView) self.snapshotView = snapshotView self.controllerView?.removeFromSuperview() @@ -141,7 +156,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll self.headerNode.controllers = [item.controller] } - func setTitleControllers(_ controllers: [ViewController]?) { + func setTitleControllers(_ controllers: [MinimizableController]?) { self.headerNode.controllers = controllers ?? [self.item.controller] } @@ -234,7 +249,10 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll if let snapshotView = self.snapshotView { var snapshotFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - snapshotView.bounds.size.width) / 2.0), y: 0.0), size: snapshotView.bounds.size) - + if self.item.controller.minimizedTopEdgeOffset == nil && isExpanded { + snapshotFrame = snapshotFrame.offsetBy(dx: 0.0, dy: -12.0) + } + var requiresBlur = false var blurFrame = snapshotFrame if snapshotView.frame.width * 1.1 < size.width { @@ -291,7 +309,12 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll private var presentationDataDisposable: Disposable? public private(set) var isExpanded: Bool = false - public var willMaximize: (() -> Void)? + public var willMaximize: ((MinimizedContainer) -> Void)? + public var willDismiss: ((MinimizedContainer) -> Void)? + public var didDismiss: ((MinimizedContainer) -> Void)? + + public private(set) var statusBarStyle: StatusBarStyle = .White + public var statusBarStyleUpdated: (() -> Void)? private let bottomEdgeView: UIImageView private let blurView: BlurView @@ -310,7 +333,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll private var isApplyingTransition = false private var validLayout: ContainerViewLayout? - public var controllers: [ViewController] { + public var controllers: [MinimizableController] { return self.items.map { $0.controller } } @@ -369,6 +392,12 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll self.scrollView.alwaysBounceVertical = true self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.shouldPassthrough = { [weak self] in + guard let self else { + return true + } + return !self.isExpanded + } let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) panGestureRecognizer.delegate = self.wrappedGestureRecognizerDelegate @@ -452,13 +481,13 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll case .changed: guard let _ = self.dismissingItemId else { return } - var delta = gestureRecognizer.translation(in: scrollView) - delta.y = 0 + var translation = gestureRecognizer.translation(in: scrollView) + translation.y = 0 if let offset = self.dismissingItemOffset { - self.dismissingItemOffset = offset + delta.x + self.dismissingItemOffset = offset + translation.x } else { - self.dismissingItemOffset = delta.x + self.dismissingItemOffset = translation.x } gestureRecognizer.setTranslation(.zero, in: scrollView) @@ -470,17 +499,38 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll if let offset = self.dismissingItemOffset { let velocity = gestureRecognizer.velocity(in: self.view) if offset < -self.frame.width / 3.0 || velocity.x < -300.0 { - self.currentTransition = .dismiss(itemId: itemId) - - self.items.removeAll(where: { $0.id == itemId }) - if self.items.count == 1 { - self.isExpanded = false - self.willMaximize?() - needsLayout = false + let proceed = { + self.currentTransition = .dismiss(itemId: itemId) + + self.items.removeAll(where: { $0.id == itemId }) + if self.items.count == 1, maximizeLastStandingController { + self.isExpanded = false + self.willMaximize?(self) + needsLayout = false + } else if self.items.count == 0 { + self.willDismiss?(self) + self.isExpanded = false + } + } + if let item = self.items.first(where: { $0.id == itemId }), !item.controller.shouldDismissImmediately() { + self.displayDismissConfirmation(completion: { commit in + self.dismissingItemOffset = nil + self.dismissingItemId = nil + if commit { + proceed() + } else { + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) + } + }) + } else { + proceed() + self.dismissingItemOffset = nil + self.dismissingItemId = nil } + } else { + self.dismissingItemOffset = nil + self.dismissingItemId = nil } - self.dismissingItemOffset = nil - self.dismissingItemId = nil } } if needsLayout { @@ -501,11 +551,12 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll return result } - public func addController(_ viewController: ViewController, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void, transition: ContainedViewLayoutTransition) { + public func addController(_ viewController: MinimizableController, topEdgeOffset: CGFloat?, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void, transition: ContainedViewLayoutTransition) { let item = Item( id: AnyHashable(Int64.random(in: Int64.min ... Int64.max)), controller: viewController, - beforeMaximize: beforeMaximize + beforeMaximize: beforeMaximize, + topEdgeOffset: topEdgeOffset ) self.items.append(item) @@ -513,6 +564,15 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll self.requestUpdate(transition: transition) } + public func removeController(_ viewController: MinimizableController) { + guard let item = self.items.first(where: { $0.controller === viewController }) else { + return + } + + self.items.removeAll(where: { $0.id == item.id }) + self.requestUpdate(transition: .animated(duration: 0.25, curve: .easeInOut)) + } + private enum Transition: Equatable { case minimize(itemId: AnyHashable) case maximize(itemId: AnyHashable) @@ -534,7 +594,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll } } - public func maximizeController(_ viewController: ViewController, animated: Bool, completion: @escaping (Bool) -> Void) { + public func maximizeController(_ viewController: MinimizableController, animated: Bool, completion: @escaping (Bool) -> Void) { guard let item = self.items.first(where: { $0.controller === viewController }) else { completion(self.items.count == 0) return @@ -570,6 +630,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll guard !self.items.isEmpty && !self.isExpanded && self.currentTransition == nil else { return } + if self.items.count == 1, let item = self.items.first { if let navigationController = self.navigationController { item.beforeMaximize(navigationController, { [weak self] in @@ -577,6 +638,12 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll }) } } else { + let contentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.bounds.height) + self.scrollView.contentOffset = CGPoint(x: 0.0, y: contentOffset) + for itemNode in self.itemNodes.values { + itemNode.frame = itemNode.frame.offsetBy(dx: 0.0, dy: contentOffset) + } + self.isExpanded = true self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) } @@ -598,18 +665,44 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll if scrollView.contentOffset.y < -64.0, let lastItemId = self.items.last?.id, let itemNode = self.itemNodes[lastItemId] { let velocity = scrollView.panGestureRecognizer.velocity(in: self.view).y let distance = layout.size.height - self.collapsedHeight(layout: layout) - itemNode.frame.minY - let initialVelocity = distance != 0.0 ? abs(velocity / distance) : 0.0 + let initialVelocity = min(8.0, distance != 0.0 ? abs(velocity / distance) : 0.0) self.isExpanded = false scrollView.isScrollEnabled = false scrollView.panGestureRecognizer.isEnabled = false scrollView.panGestureRecognizer.isEnabled = true - scrollView.contentOffset = contentOffset + scrollView.setContentOffset(contentOffset, animated: false) self.currentTransition = .collapse self.requestUpdate(transition: .animated(duration: 0.4, curve: .customSpring(damping: 180.0, initialVelocity: initialVelocity))) } } + private func displayDismissConfirmation(completion: @escaping (Bool) -> Void) { + let actionSheet = ActionSheetController(presentationData: self.presentationData) + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: self.presentationData.strings.WebApp_CloseConfirmation), + ActionSheetButtonItem(title: self.presentationData.strings.WebApp_CloseAnyway, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + completion(true) + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + completion(false) + }) + ]) + ]) + actionSheet.dismissed = { cancelled in + guard cancelled else { + return + } + completion(false) + } + self.navigationController?.presentOverlay(controller: actionSheet, inGlobal: false, blockInteraction: false) + } + private func requestUpdate(transition: ContainedViewLayoutTransition, completion: @escaping (Transition) -> Void = { _ in }) { guard let layout = self.validLayout else { return @@ -673,11 +766,15 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll var index = 0 let contentHeight = frameForIndex(index: self.items.count - 1, size: layout.size, insets: itemInsets, itemCount: self.items.count, boundingSize: layout.size).midY - 70.0 + + var effectiveScrollBounds = self.scrollView.bounds + effectiveScrollBounds.origin.y = max(0.0, min(contentHeight - self.scrollView.bounds.height, effectiveScrollBounds.origin.y)) + for item in self.items { if let currentTransition = self.currentTransition { if currentTransition.matches(item: item) { continue - } else if case .dismiss = currentTransition, self.items.count == 1 { + } else if case .dismiss = currentTransition, self.items.count == 1 && maximizeLastStandingController { continue } } @@ -694,35 +791,82 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll self.scrollView.addSubnode(itemNode) self.itemNodes[item.id] = itemNode } - itemNode.closeTapped = { [weak self] in + itemNode.closeTapped = { [weak self, weak itemNode] in guard let self else { return } if self.isExpanded { - var needsLayout = true - self.currentTransition = .dismiss(itemId: item.id) - - self.items.removeAll(where: { $0.id == item.id }) - if self.items.count == 1 { - self.isExpanded = false - self.willMaximize?() - needsLayout = false + let proceed = { [weak self] in + guard let self else { + return + } + var needsLayout = true + self.currentTransition = .dismiss(itemId: item.id) + + self.items.removeAll(where: { $0.id == item.id }) + if self.items.count == 1, maximizeLastStandingController { + self.isExpanded = false + self.willMaximize?(self) + needsLayout = false + } else if self.items.count == 0 { + self.isExpanded = false + self.willDismiss?(self) + } + if needsLayout { + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) + } } - if needsLayout { - self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) + if let item = itemNode?.item, !item.controller.shouldDismissImmediately() { + self.displayDismissConfirmation(completion: { commit in + if commit { + proceed() + } + }) + } else { + proceed() } } else { - self.navigationController?.dismissMinimizedControllers(animated: true) + if self.items.count > 1 { + let actionSheet = ActionSheetController(presentationData: self.presentationData) + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: self.presentationData.strings.WebApp_Minimized_CloseAllTitle), + ActionSheetButtonItem(title: self.presentationData.strings.WebApp_Minimized_CloseAll(Int32(self.items.count)), color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + self?.navigationController?.dismissMinimizedControllers(animated: true) + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + self.navigationController?.presentOverlay(controller: actionSheet, inGlobal: false, blockInteraction: false) + } else if let item = self.items.first { + if !item.controller.shouldDismissImmediately() { + self.displayDismissConfirmation(completion: { [weak self] commit in + if commit { + self?.navigationController?.dismissMinimizedControllers(animated: true) + } + }) + } else { + self.navigationController?.dismissMinimizedControllers(animated: true) + } + } } } - itemNode.tapped = { [weak self] in + itemNode.tapped = { [weak self, weak itemNode] in guard let self else { return } - if self.isExpanded { + if self.isExpanded, let itemNode { if let navigationController = self.navigationController { - itemNode.item.beforeMaximize(navigationController, { [weak self] in - self?.navigationController?.maximizeViewController(item.controller, animated: true) + itemNode.item.beforeMaximize(navigationController, { [weak self, weak itemNode] in + if let item = itemNode?.item { + self?.navigationController?.maximizeViewController(item.controller, animated: true) + } }) } } else { @@ -741,14 +885,14 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll if self.isExpanded { let currentItemFrame = frameForIndex(index: index, size: layout.size, insets: itemInsets, itemCount: self.items.count, boundingSize: layout.size) - let currentItemTransform = final3dTransform(for: currentItemFrame.minY, size: currentItemFrame.size, contentHeight: contentHeight, itemCount: self.items.count, additionalAngle: self.highlightedItemId == item.id ? 0.04 : nil, scrollBounds: self.scrollView.bounds, insets: itemInsets) + let currentItemTransform = final3dTransform(for: currentItemFrame.minY, size: currentItemFrame.size, contentHeight: contentHeight, itemCount: self.items.count, additionalAngle: self.highlightedItemId == item.id ? 0.04 : nil, scrollBounds: effectiveScrollBounds, insets: itemInsets) var effectiveItemFrame = currentItemFrame - var effectiveItemTransform = currentItemTransform + let effectiveItemTransform = currentItemTransform if let dismissingItemId = self.dismissingItemId, let deletingIndex = self.items.firstIndex(where: { $0.id == dismissingItemId }), let offset = self.dismissingItemOffset { - var targetItemFrame: CGRect? - var targetItemTransform: CATransform3D? +// var targetItemFrame: CGRect? +// var targetItemTransform: CATransform3D? if deletingIndex == index { let effectiveOffset: CGFloat if offset <= 0.0 { @@ -757,25 +901,26 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll effectiveOffset = scrollingRubberBandingOffset(offset: offset, bandingStart: 0.0, range: 20.0) } effectiveItemFrame = effectiveItemFrame.offsetBy(dx: effectiveOffset, dy: 0.0) - } else if index < deletingIndex { - let frame = frameForIndex(index: index, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size) - let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets) - - targetItemFrame = frame - targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets) - } else { - let frame = frameForIndex(index: index - 1, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size) - let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets) - - targetItemFrame = frame - targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets) - } + } +// else if index < deletingIndex { +// let frame = frameForIndex(index: index, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size) +// let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets) +// +// targetItemFrame = frame +// targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets) +// } else { +// let frame = frameForIndex(index: index - 1, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size) +// let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets) +// +// targetItemFrame = frame +// targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets) +// } - if let targetItemFrame, let targetItemTransform { - let fraction = max(0.0, min(1.0, -1.0 * offset / (layout.size.width * 1.5))) - effectiveItemFrame = effectiveItemFrame.interpolate(with: targetItemFrame, fraction: fraction) - effectiveItemTransform = effectiveItemTransform.interpolate(with: targetItemTransform, fraction: fraction) - } +// if let targetItemFrame, let targetItemTransform { +// let fraction = max(0.0, min(1.0, -1.0 * offset / (layout.size.width * 1.5))) +// effectiveItemFrame = effectiveItemFrame.interpolate(with: targetItemFrame, fraction: fraction) +// effectiveItemTransform = effectiveItemTransform.interpolate(with: targetItemTransform, fraction: fraction) +// } } itemFrame = effectiveItemFrame itemTransform = effectiveItemTransform @@ -786,6 +931,8 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll var hideTransform = false if let currentTransition = self.currentTransition { if case let .maximize(itemId) = currentTransition { + itemOffset += self.scrollView.bounds.origin.y + itemOffset += layout.size.height * 0.25 if let lastItemNode = self.scrollView.subviews.last?.asyncdisplaykit_node as? ItemNode, lastItemNode.item.id == itemId { hideTransform = true @@ -836,15 +983,38 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll let contentSize = CGSize(width: layout.size.width, height: contentHeight) if self.scrollView.contentSize != contentSize { + var contentSizeDelta: CGFloat? + if contentSize.height < self.scrollView.contentSize.height, transition.isAnimated { + let currentContentOffset = self.scrollView.contentOffset.y + let updatedContentOffset = max(0.0, contentSize.height - self.scrollView.bounds.height) + contentSizeDelta = currentContentOffset - updatedContentOffset + } self.scrollView.contentSize = contentSize + if let contentSizeDelta { + transition.animateBounds(layer: self.scrollView.layer, from: CGRect(origin: CGPoint(x: 0.0, y: self.scrollView.contentOffset.y + contentSizeDelta), size: self.scrollView.bounds.size)) + } } if self.scrollView.frame != bounds { self.scrollView.frame = bounds } - self.scrollView.passthrough = !self.isExpanded self.scrollView.isScrollEnabled = self.isExpanded self.expandedTapGestureRecoginzer?.isEnabled = self.isExpanded + var resolvedStatusBarStyle: StatusBarStyle = .Ignore + if self.isExpanded { + if self.scrollView.contentOffset.y > additionalInsetTop + insets.top / 2.0 { + resolvedStatusBarStyle = .Hide + } else { + resolvedStatusBarStyle = .White + } + } + if self.statusBarStyle != resolvedStatusBarStyle { + self.statusBarStyle = resolvedStatusBarStyle + Queue.mainQueue().justDispatch { + self.statusBarStyleUpdated?() + } + } + if let currentTransition = self.currentTransition { self.isApplyingTransition = true switch self.currentTransition { @@ -865,14 +1035,19 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll itemNode.animateIn() var initialOffset = insets.top - if let minimizedTopEdgeOffset = itemNode.item.controller.minimizedTopEdgeOffset { - initialOffset += minimizedTopEdgeOffset - } - if layout.size.width < layout.size.height { - initialOffset += 10.0 - } - if let minimizedBounds = itemNode.item.controller.minimizedBounds { - initialOffset += -minimizedBounds.minY + if let topEdgeOffset = itemNode.item.topEdgeOffset { + initialOffset += topEdgeOffset + dimView.removeFromSuperview() + } else { + if let minimizedTopEdgeOffset = itemNode.item.controller.minimizedTopEdgeOffset { + initialOffset += minimizedTopEdgeOffset + } + if layout.size.width < layout.size.height { + initialOffset += 10.0 + } + if let minimizedBounds = itemNode.item.controller.minimizedBounds { + initialOffset += -minimizedBounds.minY + } } transition.animatePosition(node: itemNode, from: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + initialOffset), completion: { _ in @@ -900,6 +1075,14 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll transition.updateBounds(node: itemNode, bounds: CGRect(origin: .zero, size: layout.size)) } transition.updateTransform(node: itemNode, transform: CATransform3DIdentity) + + if let _ = itemNode.snapshotView { + if itemNode.item.controller.minimizedTopEdgeOffset == nil, let snapshotView = itemNode.snapshotView, snapshotView.frame.origin.y == -12.0 { + let snapshotFrame = snapshotView.frame.offsetBy(dx: 0.0, dy: 12.0) + transition.updateFrame(view: snapshotView, frame: snapshotFrame) + } + } + transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in self.isApplyingTransition = false if self.currentTransition == currentTransition { @@ -910,10 +1093,11 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll if let _ = itemNode.snapshotView { let snapshotContainerView = itemNode.snapshotContainerView + snapshotContainerView.isUserInteractionEnabled = true snapshotContainerView.layer.allowsGroupOpacity = true snapshotContainerView.center = CGPoint(x: itemNode.item.controller.displayNode.view.bounds.width / 2.0, y: snapshotContainerView.bounds.height / 2.0) itemNode.item.controller.displayNode.view.addSubview(snapshotContainerView) - Queue.mainQueue().after(0.15, { + Queue.mainQueue().after(0.35, { snapshotContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in snapshotContainerView.removeFromSuperview() }) @@ -931,7 +1115,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll guard let dismissedItemNode = self.itemNodes[itemId] else { return } - if self.items.count == 1 { + if self.items.count == 1, maximizeLastStandingController { if let itemNode = self.itemNodes.first(where: { $0.0 != itemId })?.value, let navigationController = self.navigationController { itemNode.item.beforeMaximize(navigationController, { [weak self] in guard let self else { @@ -945,6 +1129,14 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll itemNode.animateOut() transition.updateTransform(node: itemNode, transform: CATransform3DIdentity) + + if let _ = itemNode.snapshotView { + if itemNode.item.controller.minimizedTopEdgeOffset == nil, let snapshotView = itemNode.snapshotView, snapshotView.frame.origin.y == -12.0 { + let snapshotFrame = snapshotView.frame.offsetBy(dx: 0.0, dy: 12.0) + transition.updateFrame(view: snapshotView, frame: snapshotFrame) + } + } + transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in self.isApplyingTransition = false if self.currentTransition == currentTransition { @@ -963,6 +1155,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll } transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y)) } else { + let isLast = self.items.isEmpty transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y), completion: { _ in self.isApplyingTransition = false if self.currentTransition == currentTransition { @@ -972,7 +1165,15 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll self.itemNodes[itemId] = nil dismissedItemNode.removeFromSupernode() + + if isLast { + self.didDismiss?(self) + } }) + if isLast { + let dismissOffset = collapsedHeight(layout: layout) + transition.updatePosition(layer: self.bottomEdgeView.layer, position: self.bottomEdgeView.layer.position.offsetBy(dx: 0.0, dy: dismissOffset)) + } } case .dismissAll: let dismissOffset = collapsedHeight(layout: layout) diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift index fde07311614..43e0faa569d 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift @@ -4,58 +4,112 @@ import AsyncDisplayKit import Display import SwiftSignalKit import TelegramPresentationData +import ComponentFlow +import MultilineTextComponent +import BundleIconComponent +import PlainButtonComponent + +private final class WeakController { + private weak var _value: MinimizableController? + + public var value: MinimizableController? { + return self._value + } + + public init(_ value: MinimizableController) { + self._value = value + } +} final class MinimizedHeaderNode: ASDisplayNode { var theme: NavigationControllerTheme { didSet { - self.minimizedBackgroundNode.backgroundColor = self.theme.navigationBar.opaqueBackgroundColor - self.minimizedCloseButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Close"), color: self.theme.navigationBar.primaryTextColor), for: .normal) + self.backgroundView.backgroundColor = self.theme.navigationBar.opaqueBackgroundColor + self.progressView.backgroundColor = self.theme.navigationBar.primaryTextColor.withAlphaComponent(0.06) + self.iconView.tintColor = self.theme.navigationBar.primaryTextColor } } let strings: PresentationStrings - private let minimizedBackgroundNode: ASDisplayNode - private let minimizedTitleNode: ImmediateTextNode - private let minimizedCloseButton: HighlightableButtonNode - private var minimizedTitleDisposable: Disposable? + private let backgroundView = UIView() + private let progressView = UIView() + private var iconView = UIImageView() + private let titleLabel = ComponentView() + private let closeButton = ComponentView() + private var titleDisposable: Disposable? - private var _controllers: [Weak] = [] - var controllers: [ViewController] { + private var _controllers: [WeakController] = [] + var controllers: [MinimizableController] { get { return self._controllers.compactMap { $0.value } } set { if !newValue.isEmpty { + if self.controllers.count == 1, let icon = self.controllers.first?.minimizedIcon { + self.icon = icon + } else { + self.icon = nil + } + + if self.controllers.count == 1, let progress = self.controllers.first?.minimizedProgress { + self.progress = progress + } else { + self.progress = nil + } + if newValue.count != self.controllers.count { - self._controllers = newValue.map { Weak($0) } + self._controllers = newValue.map { WeakController($0) } - self.minimizedTitleDisposable?.dispose() - self.minimizedTitleDisposable = nil + self.titleDisposable?.dispose() + self.titleDisposable = nil var signals: [Signal] = [] for controller in newValue { signals.append(controller.titleSignal) } - self.minimizedTitleDisposable = (combineLatest(signals) + self.titleDisposable = (combineLatest(signals) |> deliverOnMainQueue).start(next: { [weak self] titles in guard let self else { return } - let titles = titles.compactMap { $0 } + let titles = titles.compactMap { $0 }.filter { !$0.isEmpty } if titles.count == 1, let title = titles.first { self.title = title } else if let title = titles.last { + var trimmedTitle = title + if trimmedTitle.count > 20 { + trimmedTitle = "\(trimmedTitle.prefix(20).trimmingCharacters(in: .whitespacesAndNewlines))\u{2026}" + } let othersString = self.strings.WebApp_MinimizedTitle_Others(Int32(titles.count - 1)) - self.title = self.strings.WebApp_MinimizedTitleFormat(title, othersString).string + self.title = self.strings.WebApp_MinimizedTitleFormat(trimmedTitle, othersString).string } else { self.title = nil } }) } } else { - self.minimizedTitleDisposable?.dispose() - self.minimizedTitleDisposable = nil + self.icon = nil + + self.titleDisposable?.dispose() + self.titleDisposable = nil + } + } + } + + var icon: UIImage? { + didSet { + self.iconView.image = self.icon + if let (size, insets, isExpanded) = self.validLayout { + self.update(size: size, insets: insets, isExpanded: isExpanded, transition: .immediate) + } + } + } + + var progress: Float? { + didSet { + if let (size, insets, isExpanded) = self.validLayout { + self.update(size: size, insets: insets, isExpanded: isExpanded, transition: .immediate) } } } @@ -77,46 +131,37 @@ final class MinimizedHeaderNode: ASDisplayNode { self.theme = theme self.strings = strings - self.minimizedBackgroundNode = ASDisplayNode() - self.minimizedBackgroundNode.cornerRadius = 10.0 - self.minimizedBackgroundNode.clipsToBounds = true - self.minimizedBackgroundNode.backgroundColor = theme.navigationBar.opaqueBackgroundColor + self.backgroundView.clipsToBounds = true + self.backgroundView.backgroundColor = self.theme.navigationBar.opaqueBackgroundColor + self.backgroundView.layer.cornerRadius = 10.0 if #available(iOS 11.0, *) { - self.minimizedBackgroundNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.backgroundView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] } - - self.minimizedTitleNode = ImmediateTextNode() - self.minimizedCloseButton = HighlightableButtonNode() - self.minimizedCloseButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Close"), color: self.theme.navigationBar.primaryTextColor), for: .normal) + self.progressView.backgroundColor = self.theme.navigationBar.primaryTextColor.withAlphaComponent(0.06) + self.iconView.contentMode = .scaleAspectFit + self.iconView.clipsToBounds = true + self.iconView.layer.cornerRadius = 2.5 + self.iconView.tintColor = self.theme.navigationBar.primaryTextColor + super.init() self.clipsToBounds = true - self.addSubnode(self.minimizedBackgroundNode) - self.minimizedBackgroundNode.addSubnode(self.minimizedTitleNode) - self.minimizedBackgroundNode.addSubnode(self.minimizedCloseButton) - - self.minimizedCloseButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside) - - applySmoothRoundedCorners(self.minimizedBackgroundNode.layer) - } - - deinit { - self.minimizedTitleDisposable?.dispose() + self.view.addSubview(self.backgroundView) + self.backgroundView.addSubview(self.progressView) + self.backgroundView.addSubview(self.iconView) + + applySmoothRoundedCorners(self.backgroundView.layer) } override func didLoad() { super.didLoad() - self.minimizedBackgroundNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.maximizeTapGesture(_:)))) - } - - @objc private func closePressed() { - self.requestClose() + self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.maximizeTapGesture(_:)))) } - + @objc private func maximizeTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { let location = recognizer.location(in: self.view) @@ -130,21 +175,81 @@ final class MinimizedHeaderNode: ASDisplayNode { func update(size: CGSize, insets: UIEdgeInsets, isExpanded: Bool, transition: ContainedViewLayoutTransition) { self.validLayout = (size, insets, isExpanded) - + let headerHeight: CGFloat = 44.0 + let titleSpacing: CGFloat = 6.0 var titleSideInset: CGFloat = 56.0 if !isExpanded { titleSideInset += insets.left } - self.minimizedTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.bold(17.0), textColor: self.theme.navigationBar.primaryTextColor) + let iconSize = CGSize(width: 20.0, height: 20.0) + + let titleSize = self.titleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: self.title ?? "", font: Font.bold(17.0), textColor: self.theme.navigationBar.primaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 1) + ), + environment: {}, + containerSize: CGSize(width: size.width - titleSideInset * 2.0, height: headerHeight) + ) + + var totalWidth = titleSize.width + if isExpanded, let icon = self.icon { + self.iconView.image = icon + totalWidth += iconSize.width + titleSpacing + } else { + self.iconView.image = nil + } - let titleSize = self.minimizedTitleNode.updateLayout(CGSize(width: size.width - titleSideInset * 2.0, height: headerHeight)) - let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((headerHeight - titleSize.height) / 2.0)), size: titleSize) - self.minimizedTitleNode.bounds = CGRect(origin: .zero, size: titleFrame.size) - transition.updatePosition(node: self.minimizedTitleNode, position: titleFrame.center) - transition.updateFrame(node: self.minimizedCloseButton, frame: CGRect(origin: CGPoint(x: isExpanded ? 0.0 : insets.left, y: 0.0), size: CGSize(width: 44.0, height: 44.0))) + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0), y: floorToScreenPixels((headerHeight - iconSize.height) / 2.0)), size: iconSize) + self.iconView.frame = iconFrame - transition.updateFrame(node: self.minimizedBackgroundNode, frame: CGRect(origin: .zero, size: CGSize(width: size.width, height: 243.0))) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0) + totalWidth - titleSize.width, y: floorToScreenPixels((headerHeight - titleSize.height) / 2.0)), size: titleSize) + if let view = self.titleLabel.view { + if view.superview == nil { + self.backgroundView.addSubview(view) + } + + view.bounds = CGRect(origin: .zero, size: titleFrame.size) + transition.updatePosition(layer: view.layer, position: titleFrame.center) + } + + let _ = self.closeButton.update( + transition: .immediate, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Close", + tintColor: self.theme.navigationBar.primaryTextColor + ) + ), + effectAlignment: .center, + minSize: CGSize(width: 44.0, height: 44.0), + action: { [weak self] in + self?.requestClose() + }, + animateScale: false + ) + ), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let closeButtonFrame = CGRect(origin: CGPoint(x: isExpanded ? 0.0 : insets.left, y: 0.0), size: CGSize(width: 44.0, height: 44.0)) + if let view = self.closeButton.view { + if view.superview == nil { + self.backgroundView.addSubview(view) + } + + transition.updateFrame(view: view, frame: closeButtonFrame) + } + + transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: size.width, height: 243.0))) + + transition.updateAlpha(layer: self.progressView.layer, alpha: isExpanded && self.progress != nil ? 1.0 : 0.0) + if let progress = self.progress { + self.progressView.frame = CGRect(origin: .zero, size: CGSize(width: size.width * CGFloat(progress), height: 243.0)) + } } } diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/Utils.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/Utils.swift index ee5f726e16d..c90bad1ee5e 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/Utils.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/Utils.swift @@ -113,7 +113,10 @@ func interitemSpacing(itemCount: Int, boundingSize: CGSize, insets: UIEdgeInsets func frameForIndex(index: Int, size: CGSize, insets: UIEdgeInsets, itemCount: Int, boundingSize: CGSize) -> CGRect { let spacing = interitemSpacing(itemCount: itemCount, boundingSize: boundingSize, insets: insets) - let y = additionalInsetTop + insets.top + spacing * CGFloat(index) + var y = additionalInsetTop + insets.top + spacing * CGFloat(index) + if itemCount == 1 { + y += 72.0 + } let origin = CGPoint(x: insets.left, y: y) return CGRect(origin: origin, size: CGSize(width: size.width - insets.left - insets.right, height: size.height)) diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index 0d010bdde49..1f39885a99b 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -17,6 +17,7 @@ private var nextRenderTargetId: Int64 = 1 open class MultiAnimationRenderTarget: SimpleLayer { public let id: Int64 + public var numFrames: Int? let deinitCallbacks = Bag<() -> Void>() let updateStateCallbacks = Bag<() -> Void>() @@ -545,6 +546,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } target.contents = loadedFrame.image.cgImage + target.numFrames = item.numFrames if let blurredRepresentationTarget = target.blurredRepresentationTarget { blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage @@ -580,6 +582,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { completion(false, true) return } + target.numFrames = item.numFrames if let loadedFrame = loadedFrame { if let cgImage = loadedFrame.image.cgImage { if hadIntermediateUpdate { diff --git a/submodules/TelegramUI/Components/NavigationStackComponent/BUILD b/submodules/TelegramUI/Components/NavigationStackComponent/BUILD new file mode 100644 index 00000000000..775f9ca233d --- /dev/null +++ b/submodules/TelegramUI/Components/NavigationStackComponent/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "NavigationStackComponent", + module_name = "NavigationStackComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/AppBundle", + "//submodules/Components/BundleIconComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift b/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift new file mode 100644 index 00000000000..0a0a12c4a4b --- /dev/null +++ b/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift @@ -0,0 +1,297 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import AppBundle +import BundleIconComponent + +private final class NavigationContainer: UIView, UIGestureRecognizerDelegate { + var requestUpdate: ((ComponentTransition) -> Void)? + var requestPop: (() -> Void)? + var transitionFraction: CGFloat = 0.0 + + private var panRecognizer: InteractiveTransitionGestureRecognizer? + + var isNavigationEnabled: Bool = false { + didSet { + self.panRecognizer?.isEnabled = self.isNavigationEnabled + } + } + + init() { + super.init(frame: .zero) + + let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in + guard let strongSelf = self else { + return [] + } + let _ = strongSelf + return [.right] + }) + panRecognizer.delegate = self + self.addGestureRecognizer(panRecognizer) + self.panRecognizer = panRecognizer + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer { + return false + } + if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { + return true + } + return false + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.transitionFraction = 0.0 + case .changed: + let distanceFactor: CGFloat = recognizer.translation(in: self).x / self.bounds.width + let transitionFraction = max(0.0, min(1.0, distanceFactor)) + if self.transitionFraction != transitionFraction { + self.transitionFraction = transitionFraction + self.requestUpdate?(.immediate) + } + case .ended, .cancelled: + let distanceFactor: CGFloat = recognizer.translation(in: self).x / self.bounds.width + let transitionFraction = max(0.0, min(1.0, distanceFactor)) + if transitionFraction > 0.2 { + self.transitionFraction = 0.0 + self.requestPop?() + } else { + self.transitionFraction = 0.0 + self.requestUpdate?(.spring(duration: 0.45)) + } + default: + break + } + } +} + +public final class NavigationStackComponent: Component { + public let items: [AnyComponentWithIdentity] + public let requestPop: () -> Void + + public init( + items: [AnyComponentWithIdentity], + requestPop: @escaping () -> Void + ) { + self.items = items + self.requestPop = requestPop + } + + public static func ==(lhs: NavigationStackComponent, rhs: NavigationStackComponent) -> Bool { + if lhs.items != rhs.items { + return false + } + return true + } + + private final class ItemView: UIView { + let contents = ComponentView() + let dimView = UIView() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.dimView.alpha = 0.0 + self.dimView.backgroundColor = UIColor.black.withAlphaComponent(0.2) + self.dimView.isUserInteractionEnabled = false + self.addSubview(self.dimView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + + private struct ReadyItem { + var index: Int + var itemId: AnyHashable + var itemView: ItemView + var itemTransition: ComponentTransition + var itemSize: CGSize + + init(index: Int, itemId: AnyHashable, itemView: ItemView, itemTransition: ComponentTransition, itemSize: CGSize) { + self.index = index + self.itemId = itemId + self.itemView = itemView + self.itemTransition = itemTransition + self.itemSize = itemSize + } + } + + public final class View: UIView { + private var itemViews: [AnyHashable: ItemView] = [:] + private let navigationContainer = NavigationContainer() + + private var component: NavigationStackComponent? + private var state: EmptyComponentState? + + public override init(frame: CGRect) { + super.init(frame: CGRect()) + + self.addSubview(self.navigationContainer) + + self.navigationContainer.requestUpdate = { [weak self] transition in + guard let self else { + return + } + self.state?.updated(transition: transition) + } + + self.navigationContainer.requestPop = { [weak self] in + guard let self else { + return + } + self.component?.requestPop() + } + } + + required public init?(coder: NSCoder) { + preconditionFailure() + } + + func update(component: NavigationStackComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let navigationTransitionFraction = self.navigationContainer.transitionFraction + self.navigationContainer.isNavigationEnabled = component.items.count > 1 + + var validItemIds: [AnyHashable] = [] + + var readyItems: [ReadyItem] = [] + for i in 0 ..< component.items.count { + let item = component.items[i] + let itemId = item.id + validItemIds.append(itemId) + + let itemView: ItemView + var itemTransition = transition + if let current = self.itemViews[itemId] { + itemView = current + } else { + itemTransition = itemTransition.withAnimation(.none) + itemView = ItemView() + self.itemViews[itemId] = itemView + itemView.contents.parentState = state + } + + let itemSize = itemView.contents.update( + transition: itemTransition, + component: item.component, + environment: { environment[ChildEnvironment.self] }, + containerSize: CGSize(width: availableSize.width, height: availableSize.height) + ) + + readyItems.append(ReadyItem( + index: i, + itemId: itemId, + itemView: itemView, + itemTransition: itemTransition, + itemSize: itemSize + )) + } + + let sortedItems = readyItems.sorted(by: { $0.index < $1.index }) + for readyItem in sortedItems { + let transitionFraction: CGFloat + let alphaTransitionFraction: CGFloat + if readyItem.index == readyItems.count - 1 { + transitionFraction = navigationTransitionFraction + alphaTransitionFraction = 1.0 + } else if readyItem.index == readyItems.count - 2 { + transitionFraction = navigationTransitionFraction - 1.0 + alphaTransitionFraction = navigationTransitionFraction + } else { + transitionFraction = 0.0 + alphaTransitionFraction = 0.0 + } + + let transitionOffset: CGFloat + if readyItem.index == readyItems.count - 1 { + transitionOffset = readyItem.itemSize.width * transitionFraction + } else { + transitionOffset = readyItem.itemSize.width / 3.0 * transitionFraction + } + + let itemFrame = CGRect(origin: CGPoint(x: transitionOffset, y: 0.0), size: readyItem.itemSize) + + let itemBounds = CGRect(origin: .zero, size: itemFrame.size) + if let itemComponentView = readyItem.itemView.contents.view { + var isAdded = false + if itemComponentView.superview == nil { + isAdded = true + + readyItem.itemView.insertSubview(itemComponentView, at: 0) + self.navigationContainer.addSubview(readyItem.itemView) + } + readyItem.itemTransition.setFrame(view: readyItem.itemView, frame: itemFrame) + readyItem.itemTransition.setFrame(view: itemComponentView, frame: itemBounds) + readyItem.itemTransition.setFrame(view: readyItem.itemView.dimView, frame: CGRect(origin: .zero, size: availableSize)) + readyItem.itemTransition.setAlpha(view: readyItem.itemView.dimView, alpha: 1.0 - alphaTransitionFraction) + + if readyItem.index > 0 && isAdded { + transition.animatePosition(view: itemComponentView, from: CGPoint(x: itemFrame.width, y: 0.0), to: .zero, additive: true, completion: nil) + } + } + } + + let lastHeight = sortedItems.last?.itemSize.height ?? 0.0 + let previousHeight: CGFloat + if sortedItems.count > 1 { + previousHeight = sortedItems[sortedItems.count - 2].itemSize.height + } else { + previousHeight = lastHeight + } + let contentHeight = lastHeight * (1.0 - navigationTransitionFraction) + previousHeight * navigationTransitionFraction + + var removedItemIds: [AnyHashable] = [] + for (id, _) in self.itemViews { + if !validItemIds.contains(id) { + removedItemIds.append(id) + } + } + for id in removedItemIds { + guard let itemView = self.itemViews[id] else { + continue + } + if let itemComponeentView = itemView.contents.view { + var position = itemComponeentView.center + position.x += itemComponeentView.bounds.width + transition.setPosition(view: itemComponeentView, position: position, completion: { _ in + itemView.removeFromSuperview() + self.itemViews.removeValue(forKey: id) + }) + } else { + itemView.removeFromSuperview() + self.itemViews.removeValue(forKey: id) + } + } + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + self.navigationContainer.frame = CGRect(origin: .zero, size: contentSize) + + return contentSize + } + } + + 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, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift index eca6988d5e8..31c385a7fca 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift @@ -194,7 +194,7 @@ public final class EmojiSelectionComponent: Component { component.backspace?() AudioServicesPlaySystemSound(1155) } - ).withHoldAction({ [weak self] in + ).withHoldAction({ [weak self] _ in guard let self, let component = self.component else { return } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index ba062d1d2af..28d1151a160 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -868,7 +868,7 @@ final class PeerAllowedReactionsScreenComponent: Component { } contentHeight += reactionCountSectionSize.height - if "".isEmpty { + if !"".isEmpty { contentHeight += 32.0 let paidReactionsSection: ComponentView diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift index 5b8d3acff0c..0306135d387 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift @@ -6,6 +6,7 @@ import Display import TelegramPresentationData public enum PeerInfoPaneKey: Int32 { + case botPreview case members case stories case storyArchive diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index 5f5401456d1..62c7351e08f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -154,9 +154,12 @@ swift_library( "//submodules/TelegramUI/Components/TextLoadingEffect", "//submodules/TelegramUI/Components/Settings/BirthdayPickerScreen", "//submodules/TelegramUI/Components/Settings/PeerSelectionScreen", + "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/ConfettiEffect", "//submodules/ContactsPeerItem", "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/UrlHandling", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenActionItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenActionItem.swift index 828a840b8e5..4114c252b05 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenActionItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenActionItem.swift @@ -3,6 +3,7 @@ import Display import SwiftSignalKit import TelegramPresentationData import AvatarNode +import AccountContext enum PeerInfoScreenActionColor { case accent @@ -89,7 +90,7 @@ private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode { self.iconDisposable.dispose() } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenActionItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift index 3dc8c142834..3461f6182b8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift @@ -170,7 +170,7 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode { } } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenAddressItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBirthdatePickerItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBirthdatePickerItem.swift index 883ceb5d289..381d450fa40 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBirthdatePickerItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBirthdatePickerItem.swift @@ -52,7 +52,7 @@ private final class PeerInfoScreenBirthdatePickerItemNode: PeerInfoScreenItemNod self.addSubnode(self.maskNode) } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenBirthdatePickerItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift index c0b3d9671a4..9f18816812d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift @@ -12,6 +12,7 @@ import ComponentFlow import MultilineTextComponent import BundleIconComponent import PlainButtonComponent +import AccountContext func businessHoursTextToCopy(businessHours: TelegramBusinessHours, presentationData: PresentationData, displayLocalTimezone: Bool) -> String { var text = "" @@ -279,7 +280,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode } } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenBusinessHoursItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCallListItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCallListItem.swift index 40adb5bbea3..1f20c7b4b92 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCallListItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCallListItem.swift @@ -57,7 +57,7 @@ private final class PeerInfoScreenCallListItemNode: PeerInfoScreenItemNode { self.addSubnode(self.maskNode) } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenCallListItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommentItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommentItem.swift index 59374afed50..ec69eaef716 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommentItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommentItem.swift @@ -3,6 +3,7 @@ import Display import TelegramPresentationData import TextFormat import Markdown +import AccountContext final class PeerInfoScreenCommentItem: PeerInfoScreenItem { enum LinkAction { @@ -63,7 +64,7 @@ private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode { self.view.addGestureRecognizer(recognizer) } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenCommentItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenContactInfoItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenContactInfoItem.swift index 0144179eb7d..1d6c028f044 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenContactInfoItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenContactInfoItem.swift @@ -237,7 +237,7 @@ private final class PeerInfoScreenContactInfoItemNode: PeerInfoScreenItemNode { return nil } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenContactInfoItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift index d05a1a25fc7..87436a17f4c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift @@ -3,6 +3,7 @@ import Display import TelegramPresentationData import EncryptionKeyVisualization import TelegramCore +import AccountContext final class PeerInfoScreenDisclosureEncryptionKeyItem: PeerInfoScreenItem { let id: AnyHashable @@ -71,7 +72,7 @@ private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScree self.addSubnode(self.maskNode) } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenDisclosureEncryptionKeyItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift index d9d6ad497ca..c8e725e618d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift @@ -2,6 +2,8 @@ import AsyncDisplayKit import Display import SwiftSignalKit import TelegramPresentationData +import TextNodeWithEntities +import AccountContext final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { enum Label { @@ -12,6 +14,7 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { case none case text(String) + case attributedText(NSAttributedString) case coloredText(String, LabelColor) case badge(String, UIColor) case semitransparentBadge(String, UIColor) @@ -22,6 +25,8 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { switch self { case .none, .image: return "" + case let .attributedText(text): + return text.string case let .text(text), let .coloredText(text, _), let .badge(text, _), let .semitransparentBadge(text, _), let .titleBadge(text, _): return text } @@ -29,7 +34,7 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { var badgeColor: UIColor? { switch self { - case .none, .text, .coloredText, .image: + case .none, .text, .coloredText, .image, .attributedText: return nil case let .badge(_, color), let .semitransparentBadge(_, color), let .titleBadge(_, color): return color @@ -69,7 +74,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { private let maskNode: ASImageNode private let iconNode: ASImageNode private let labelBadgeNode: ASImageNode - private let labelNode: ImmediateTextNode + private let labelNode: ImmediateTextNodeWithEntities private var additionalLabelNode: ImmediateTextNode? private var additionalLabelBadgeNode: ASImageNode? private let textNode: ImmediateTextNode @@ -97,7 +102,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { self.labelBadgeNode.displaysAsynchronously = false self.labelBadgeNode.isLayerBacked = true - self.labelNode = ImmediateTextNode() + self.labelNode = ImmediateTextNodeWithEntities() self.labelNode.displaysAsynchronously = false self.labelNode.isUserInteractionEnabled = false @@ -135,7 +140,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { self.iconDisposable.dispose() } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenDisclosureItem else { return 10.0 } @@ -177,8 +182,20 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { labelColorValue = presentationData.theme.list.itemSecondaryTextColor labelFont = titleFont } - self.labelNode.attributedText = NSAttributedString(string: item.label.text, font: labelFont, textColor: labelColorValue) + self.labelNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: .clear, + attemptSynchronous: true + ) + + if case let .attributedText(text) = item.label { + self.labelNode.attributedText = text + } else { + self.labelNode.attributedText = NSAttributedString(string: item.label.text, font: labelFont, textColor: labelColorValue) + } self.textNode.maximumNumberOfLines = 1 self.textNode.attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: textColorValue) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenHeaderItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenHeaderItem.swift index 05f39366a2e..eb69f67ad2d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenHeaderItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenHeaderItem.swift @@ -1,6 +1,7 @@ import AsyncDisplayKit import Display import TelegramPresentationData +import AccountContext final class PeerInfoScreenHeaderItem: PeerInfoScreenItem { let id: AnyHashable @@ -44,7 +45,7 @@ private final class PeerInfoScreenHeaderItemNode: PeerInfoScreenItemNode { self.addSubnode(self.activateArea) } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenHeaderItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift index 23707c5a9e3..71cb89c1e13 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift @@ -55,7 +55,7 @@ private final class PeerInfoScreenInfoItemNode: PeerInfoScreenItemNode { self.addSubnode(self.bottomSeparatorNode) } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenInfoItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift index 94d2ba03b25..ac992e56b06 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -11,6 +11,9 @@ import ContextUI import SwiftSignalKit import TextLoadingEffect import EmojiTextAttachmentView +import ComponentFlow +import ButtonComponent +import ComponentDisplayAdapters enum PeerInfoScreenLabeledValueTextColor { case primary @@ -52,6 +55,16 @@ private struct TextLinkItemSource: Equatable { } final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { + final class Button { + let title: String + let action: () -> Void + + init(title: String, action: @escaping () -> Void) { + self.title = title + self.action = action + } + } + let id: AnyHashable let context: AccountContext? let label: String @@ -65,8 +78,9 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { let longTapAction: ((ASDisplayNode) -> Void)? let linkItemAction: ((TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?, Promise?) -> Void)? let iconAction: (() -> Void)? + let button: Button? let contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? - let requestLayout: () -> Void + let requestLayout: (Bool) -> Void init( id: AnyHashable, @@ -82,8 +96,9 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { longTapAction: ((ASDisplayNode) -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?, Promise?) -> Void)? = nil, iconAction: (() -> Void)? = nil, + button: Button? = nil, contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil, - requestLayout: @escaping () -> Void + requestLayout: @escaping (Bool) -> Void ) { self.id = id self.context = context @@ -98,6 +113,7 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { self.longTapAction = longTapAction self.linkItemAction = linkItemAction self.iconAction = iconAction + self.button = button self.contextAction = contextAction self.requestLayout = requestLayout } @@ -124,6 +140,8 @@ private func generateExpandBackground(size: CGSize, color: UIColor) -> UIImage? } private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { + private weak var context: AccountContext? + private let containerNode: ContextControllerSourceNode private let contextSourceNode: ContextExtractedContentContainingNode @@ -151,6 +169,8 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { private var linkHighlightingNode: LinkHighlightingNode? + private var actionButton: ComponentView? + private let activateArea: AccessibilityAreaNode private var validLayout: (width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool)? @@ -319,7 +339,7 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { @objc private func expandPressed() { self.isExpanded = true - self.item?.requestLayout() + self.item?.requestLayout(true) } @objc private func iconPressed() { @@ -386,8 +406,8 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { if self.linkItemWithProgress != currentLinkItem { self.linkItemWithProgress = currentLinkItem - if let validLayout = self.validLayout { - let _ = self.update(width: validLayout.width, safeInsets: validLayout.safeInsets, presentationData: validLayout.presentationData, item: validLayout.item, topItem: validLayout.topItem, bottomItem: validLayout.bottomItem, hasCorners: validLayout.hasCorners, transition: .immediate) + if let validLayout = self.validLayout, let context = self.context { + let _ = self.update(context: context, width: validLayout.width, safeInsets: validLayout.safeInsets, presentationData: validLayout.presentationData, item: validLayout.item, topItem: validLayout.topItem, bottomItem: validLayout.bottomItem, hasCorners: validLayout.hasCorners, transition: .immediate) } } }) @@ -415,8 +435,8 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { if self.linkItemWithProgress != currentLinkItem { self.linkItemWithProgress = currentLinkItem - if let validLayout = self.validLayout { - let _ = self.update(width: validLayout.width, safeInsets: validLayout.safeInsets, presentationData: validLayout.presentationData, item: validLayout.item, topItem: validLayout.topItem, bottomItem: validLayout.bottomItem, hasCorners: validLayout.hasCorners, transition: .immediate) + if let validLayout = self.validLayout, let context = self.context { + let _ = self.update(context: context, width: validLayout.width, safeInsets: validLayout.safeInsets, presentationData: validLayout.presentationData, item: validLayout.item, topItem: validLayout.topItem, bottomItem: validLayout.bottomItem, hasCorners: validLayout.hasCorners, transition: .immediate) } } }) @@ -433,11 +453,12 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { } } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenLabeledValueItem else { return 10.0 } + self.context = context self.validLayout = (width, safeInsets, presentationData, item, topItem, bottomItem, hasCorners) self.item = item @@ -664,6 +685,56 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { height += additionalTextSize.height + 3.0 } + if let button = item.button { + if textSize.height > 0.0 { + height += 3.0 + } else { + height -= 7.0 + } + + let actionButton: ComponentView + if let current = self.actionButton { + actionButton = current + } else { + actionButton = ComponentView() + self.actionButton = actionButton + } + + let actionButtonSize = actionButton.update( + transition: ComponentTransition(transition), + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: presentationData.theme.list.itemCheckColors.fillColor, + foreground: presentationData.theme.list.itemCheckColors.foregroundColor, + pressedColor: presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: button.title, font: Font.semibold(17.0), color: presentationData.theme.list.itemCheckColors.foregroundColor))), + isEnabled: true, + allowActionWhenDisabled: false, + displaysProgress: false, + action: { + button.action() + } + )), + environment: {}, + containerSize: CGSize(width: width - sideInset * 2.0, height: 50.0) + ) + let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: height), size: actionButtonSize) + if let actionButtonView = actionButton.view { + if actionButtonView.superview == nil { + self.contextSourceNode.contentNode.view.addSubview(actionButtonView) + } + transition.updateFrame(view: actionButtonView, frame: actionButtonFrame) + } + height += actionButtonSize.height + height += 16.0 + } else { + if let actionButton = self.actionButton { + self.actionButton = nil + actionButton.view?.removeFromSuperview() + } + } + let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition) transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset))) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift index 436a667ed99..33af7c50daa 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift @@ -118,7 +118,7 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { } } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenMemberItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift index f269e27aec4..ed6532694a2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift @@ -176,7 +176,7 @@ public final class LoadingOverlayNode: ASDisplayNode { let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) - let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", 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)) + let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", 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 timestamp1: Int32 = 100000 let peers: [EnginePeer.Id: EnginePeer] = [:] let interaction = ChatListNodeInteraction(context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in @@ -416,7 +416,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod } } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenPersonalChannelItem else { return 50.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenSwitchItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenSwitchItem.swift index d62ca443546..cc01acfe54d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenSwitchItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenSwitchItem.swift @@ -2,6 +2,7 @@ import AsyncDisplayKit import Display import TelegramPresentationData import AppBundle +import AccountContext final class PeerInfoScreenSwitchItem: PeerInfoScreenItem { let id: AnyHashable @@ -89,7 +90,7 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode { } } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenSwitchItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift index 15be3b375fa..a2ddf1c7680 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift @@ -368,7 +368,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { } if let id = state.id as? PeerMessagesMediaPlaylistItemId, let playlistLocation = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation, case let .messages(chatLocation, _, _) = playlistLocation { if type == .music { - let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: .peer(id: id.messageId.peerId), subject: nil, chatLocationContextHolder: Atomic(value: nil), tag: .tag(MessageTags.music)) + let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: .peer(id: id.messageId.peerId), subject: nil, chatLocationContextHolder: Atomic(value: nil), tag: .tag(MessageTags.music)) var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift index 1d61323f753..8922ed2a8d1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift @@ -327,7 +327,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0)) markupNode.updateVisibility(true) } else if threadInfo == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) { - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil)])) + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { self.videoNode?.removeFromSupernode() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 714a60105a1..ca0a0de2a5e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -33,6 +33,7 @@ final class PeerInfoState { let isEditing: Bool let selectedMessageIds: Set? let selectedStoryIds: Set? + let paneIsReordering: Bool let updatingAvatar: PeerInfoUpdatingAvatar? let updatingBio: String? let avatarUploadProgress: AvatarUploadProgress? @@ -45,6 +46,7 @@ final class PeerInfoState { isEditing: Bool, selectedMessageIds: Set?, selectedStoryIds: Set?, + paneIsReordering: Bool, updatingAvatar: PeerInfoUpdatingAvatar?, updatingBio: String?, avatarUploadProgress: AvatarUploadProgress?, @@ -56,6 +58,7 @@ final class PeerInfoState { self.isEditing = isEditing self.selectedMessageIds = selectedMessageIds self.selectedStoryIds = selectedStoryIds + self.paneIsReordering = paneIsReordering self.updatingAvatar = updatingAvatar self.updatingBio = updatingBio self.avatarUploadProgress = avatarUploadProgress @@ -70,6 +73,7 @@ final class PeerInfoState { isEditing: isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -85,6 +89,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -100,6 +105,23 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: selectedStoryIds, + paneIsReordering: self.paneIsReordering, + updatingAvatar: self.updatingAvatar, + updatingBio: self.updatingBio, + avatarUploadProgress: self.avatarUploadProgress, + highlightedButton: self.highlightedButton, + isEditingBirthDate: self.isEditingBirthDate, + updatingBirthDate: self.updatingBirthDate, + personalChannels: self.personalChannels + ) + } + + func withPaneIsReordering(_ paneIsReordering: Bool) -> PeerInfoState { + return PeerInfoState( + isEditing: self.isEditing, + selectedMessageIds: self.selectedMessageIds, + selectedStoryIds: self.selectedStoryIds, + paneIsReordering: paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -115,6 +137,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -130,6 +153,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -145,6 +169,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: avatarUploadProgress, @@ -160,6 +185,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -175,6 +201,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -190,6 +217,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -205,6 +233,7 @@ final class PeerInfoState { isEditing: self.isEditing, selectedMessageIds: self.selectedMessageIds, selectedStoryIds: self.selectedStoryIds, + paneIsReordering: self.paneIsReordering, updatingAvatar: self.updatingAvatar, updatingBio: self.updatingBio, avatarUploadProgress: self.avatarUploadProgress, @@ -339,6 +368,7 @@ final class PeerInfoScreenData { let members: PeerInfoMembersData? let storyListContext: StoryListContext? let storyArchiveListContext: StoryListContext? + let botPreviewStoryListContext: StoryListContext? let encryptionKeyFingerprint: SecretChatKeyFingerprint? let globalSettings: TelegramGlobalSettings? let invitations: PeerExportedInvitationsState? @@ -349,11 +379,13 @@ final class PeerInfoScreenData { let isPowerSavingEnabled: Bool? let accountIsPremium: Bool let hasSavedMessageTags: Bool + let hasBotPreviewItems: Bool let isPremiumRequiredForStoryPosting: Bool let personalChannel: PeerInfoPersonalChannelData? let starsState: StarsContext.State? let starsRevenueStatsState: StarsRevenueStats? let starsRevenueStatsContext: StarsRevenueStatsContext? + let revenueStatsState: RevenueStats? let _isContact: Bool var forceIsContact: Bool = false @@ -382,6 +414,7 @@ final class PeerInfoScreenData { members: PeerInfoMembersData?, storyListContext: StoryListContext?, storyArchiveListContext: StoryListContext?, + botPreviewStoryListContext: StoryListContext?, encryptionKeyFingerprint: SecretChatKeyFingerprint?, globalSettings: TelegramGlobalSettings?, invitations: PeerExportedInvitationsState?, @@ -392,11 +425,13 @@ final class PeerInfoScreenData { isPowerSavingEnabled: Bool?, accountIsPremium: Bool, hasSavedMessageTags: Bool, + hasBotPreviewItems: Bool, isPremiumRequiredForStoryPosting: Bool, personalChannel: PeerInfoPersonalChannelData?, starsState: StarsContext.State?, starsRevenueStatsState: StarsRevenueStats?, - starsRevenueStatsContext: StarsRevenueStatsContext? + starsRevenueStatsContext: StarsRevenueStatsContext?, + revenueStatsState: RevenueStats? ) { self.peer = peer self.chatPeer = chatPeer @@ -413,6 +448,7 @@ final class PeerInfoScreenData { self.members = members self.storyListContext = storyListContext self.storyArchiveListContext = storyArchiveListContext + self.botPreviewStoryListContext = botPreviewStoryListContext self.encryptionKeyFingerprint = encryptionKeyFingerprint self.globalSettings = globalSettings self.invitations = invitations @@ -423,11 +459,13 @@ final class PeerInfoScreenData { self.isPowerSavingEnabled = isPowerSavingEnabled self.accountIsPremium = accountIsPremium self.hasSavedMessageTags = hasSavedMessageTags + self.hasBotPreviewItems = hasBotPreviewItems self.isPremiumRequiredForStoryPosting = isPremiumRequiredForStoryPosting self.personalChannel = personalChannel self.starsState = starsState self.starsRevenueStatsState = starsRevenueStatsState self.starsRevenueStatsContext = starsRevenueStatsContext + self.revenueStatsState = revenueStatsState } } @@ -909,6 +947,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, members: nil, storyListContext: hasStories == true ? storyListContext : nil, storyArchiveListContext: nil, + botPreviewStoryListContext: nil, encryptionKeyFingerprint: nil, globalSettings: globalSettings, invitations: nil, @@ -919,11 +958,13 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, isPowerSavingEnabled: isPowerSavingEnabled, accountIsPremium: peer?.isPremium ?? false, hasSavedMessageTags: false, + hasBotPreviewItems: false, isPremiumRequiredForStoryPosting: true, personalChannel: personalChannel, starsState: starsState, starsRevenueStatsState: nil, - starsRevenueStatsContext: nil + starsRevenueStatsContext: nil, + revenueStatsState: nil ) } } @@ -951,6 +992,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen members: nil, storyListContext: nil, storyArchiveListContext: nil, + botPreviewStoryListContext: nil, encryptionKeyFingerprint: nil, globalSettings: nil, invitations: nil, @@ -961,11 +1003,13 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen isPowerSavingEnabled: nil, accountIsPremium: false, hasSavedMessageTags: false, + hasBotPreviewItems: false, isPremiumRequiredForStoryPosting: true, personalChannel: nil, starsState: nil, starsRevenueStatsState: nil, - starsRevenueStatsContext: nil + starsRevenueStatsContext: nil, + revenueStatsState: nil )) case let .user(userPeerId, secretChatId, kind): let groupsInCommon: GroupsInCommonContext? @@ -980,7 +1024,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen enum StatusInputData: Equatable { case none case presence(TelegramUserPresence) - case bot + case bot(subscriberCount: Int32?) case support } let status = Signal { subscriber in @@ -1023,7 +1067,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen return .support } if user.botInfo != nil { - return .bot + return .bot(subscriberCount: user.subscriberCount) } guard let presence = view.peerPresences[userPeerId] as? TelegramUserPresence else { return .none @@ -1032,8 +1076,12 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } |> distinctUntilChanged).start(next: { inputData in switch inputData { - case .bot: - subscriber.putNext(PeerInfoStatusData(text: strings.Bot_GenericBotStatus, isActivity: false, key: nil)) + case let .bot(subscriberCount): + if let subscriberCount, subscriberCount > 0 { + subscriber.putNext(PeerInfoStatusData(text: strings.Conversation_StatusBotSubscribers(subscriberCount), isActivity: false, key: nil)) + } else { + subscriber.putNext(PeerInfoStatusData(text: strings.Bot_GenericBotStatus, isActivity: false, key: nil)) + } case .support: subscriber.putNext(PeerInfoStatusData(text: strings.Bot_GenericSupportStatus, isActivity: false, key: nil)) default: @@ -1100,6 +1148,20 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasStoryArchive = .single(false) } + var botPreviewStoryListContext: StoryListContext? + let hasBotPreviewItems: Signal + if case .bot = kind { + let botPreviewStoryListContextValue = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: peerId, language: nil, assumeEmpty: false) + botPreviewStoryListContext = botPreviewStoryListContextValue + hasBotPreviewItems = botPreviewStoryListContextValue.state + |> map { state in + return !state.items.isEmpty + } + |> distinctUntilChanged + } else { + hasBotPreviewItems = .single(false) + } + let accountIsPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> map { peer -> Bool in return peer?.isPremium ?? false @@ -1182,8 +1244,17 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessageTags = .single(false) } - let starsRevenueStatsContextPromise = Promise(nil) - let starsRevenueStatsStatePromise = Promise(nil) + let starsRevenueContextAndState = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in + guard let peer, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) || context.sharedContext.applicationBindings.appBuildType == .internal else { + return .single((nil, nil)) + } + let starsRevenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: peerId) + return starsRevenueStatsContext.state + |> map { state -> (StarsRevenueStatsContext?, StarsRevenueStats?) in + return (starsRevenueStatsContext, state.stats) + } + } return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), @@ -1198,12 +1269,12 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, + hasBotPreviewItems, peerInfoPersonalChannel(context: context, peerId: peerId, isSettings: false), privacySettings, - starsRevenueStatsContextPromise.get(), - starsRevenueStatsStatePromise.get() + starsRevenueContextAndState ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, personalChannel, privacySettings, currentStarsRevenueStatsContext, starsRevenueStatsState -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState -> PeerInfoScreenData in var availablePanes = availablePanes if isMyProfile { availablePanes?.insert(.stories, at: 0) @@ -1236,6 +1307,12 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen availablePanes = availablePanesValue } } + + if let user = peerView.peers[peerView.peerId] as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), botInfo.flags.contains(.canEdit) { + availablePanes?.insert(.botPreview, at: 0) + } else if let cachedData = peerView.cachedData as? CachedUserData, let botPreview = cachedData.botPreview, !botPreview.items.isEmpty { + availablePanes?.insert(.botPreview, at: 0) + } } } else { availablePanes = nil @@ -1269,14 +1346,6 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen enableQRLogin: false) } - if case .bot = kind, let user = peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { - if currentStarsRevenueStatsContext == nil { - let starsRevenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: peerId) - starsRevenueStatsContextPromise.set(.single(starsRevenueStatsContext)) - starsRevenueStatsStatePromise.set(starsRevenueStatsContext.state |> map { $0.stats }) - } - } - return PeerInfoScreenData( peer: peer, chatPeer: peerView.peers[peerId], @@ -1293,6 +1362,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen members: nil, storyListContext: storyListContext, storyArchiveListContext: storyArchiveListContext, + botPreviewStoryListContext: botPreviewStoryListContext, encryptionKeyFingerprint: encryptionKeyFingerprint, globalSettings: globalSettings, invitations: nil, @@ -1303,11 +1373,13 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen isPowerSavingEnabled: nil, accountIsPremium: accountIsPremium, hasSavedMessageTags: hasSavedMessageTags, + hasBotPreviewItems: hasBotPreviewItems, isPremiumRequiredForStoryPosting: false, personalChannel: personalChannel, starsState: nil, - starsRevenueStatsState: starsRevenueStatsState, - starsRevenueStatsContext: currentStarsRevenueStatsContext + starsRevenueStatsState: starsRevenueContextAndState.1, + starsRevenueStatsContext: starsRevenueContextAndState.0, + revenueStatsState: nil ) } case .channel: @@ -1383,6 +1455,36 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let isPremiumRequiredForStoryPosting: Signal = isPremiumRequiredForStoryPosting(context: context) + let starsRevenueContextAndState = context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.CanViewStarsRevenue(id: peerId) + ) + |> distinctUntilChanged + |> mapToSignal { canViewStarsRevenue -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in + guard canViewStarsRevenue else { + return .single((nil, nil)) + } + let starsRevenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: peerId) + return starsRevenueStatsContext.state + |> map { state -> (StarsRevenueStatsContext?, StarsRevenueStats?) in + return (starsRevenueStatsContext, state.stats) + } + } + + let revenueContextAndState = context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId) + ) + |> distinctUntilChanged + |> mapToSignal { canViewRevenue -> Signal<(RevenueStatsContext?, RevenueStats?), NoError> in + guard canViewRevenue else { + return .single((nil, nil)) + } + let revenueStatsContext = RevenueStatsContext(account: context.account, peerId: peerId) + return revenueStatsContext.state + |> map { state -> (RevenueStatsContext?, RevenueStats?) in + return (revenueStatsContext, state.stats) + } + } + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: false, chatLocationContextHolder: chatLocationContextHolder), @@ -1398,9 +1500,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, - isPremiumRequiredForStoryPosting + isPremiumRequiredForStoryPosting, + starsRevenueContextAndState, + revenueContextAndState ) - |> map { peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState, revenueContextAndState -> PeerInfoScreenData in var availablePanes = availablePanes if let hasStories { if hasStories { @@ -1450,7 +1554,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen requestsStatePromise.set(requestsContext.state |> map(Optional.init)) } } - + return PeerInfoScreenData( peer: peerView.peers[peerId], chatPeer: peerView.peers[peerId], @@ -1467,6 +1571,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen members: nil, storyListContext: storyListContext, storyArchiveListContext: nil, + botPreviewStoryListContext: nil, encryptionKeyFingerprint: nil, globalSettings: nil, invitations: invitations, @@ -1477,11 +1582,13 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen isPowerSavingEnabled: nil, accountIsPremium: accountIsPremium, hasSavedMessageTags: hasSavedMessageTags, + hasBotPreviewItems: false, isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting, personalChannel: nil, starsState: nil, - starsRevenueStatsState: nil, - starsRevenueStatsContext: nil + starsRevenueStatsState: starsRevenueContextAndState.1, + starsRevenueStatsContext: starsRevenueContextAndState.0, + revenueStatsState: revenueContextAndState.1 ) } case let .group(groupId): @@ -1764,6 +1871,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen members: membersData, storyListContext: storyListContext, storyArchiveListContext: nil, + botPreviewStoryListContext: nil, encryptionKeyFingerprint: nil, globalSettings: nil, invitations: invitations, @@ -1774,11 +1882,13 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen isPowerSavingEnabled: nil, accountIsPremium: accountIsPremium, hasSavedMessageTags: hasSavedMessageTags, + hasBotPreviewItems: false, isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting, personalChannel: nil, starsState: nil, starsRevenueStatsState: nil, - starsRevenueStatsContext: nil + starsRevenueStatsContext: nil, + revenueStatsState: nil )) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift index 909f5357498..a2e196d1e4c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift @@ -162,7 +162,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { markupNode.removeFromSupernode() } - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil)])) + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil)])) let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { self.videoNode?.removeFromSupernode() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 447b807ec82..dd48610be3b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -404,10 +404,10 @@ private final class PeerInfoPendingPane { ensureRectVisible: @escaping (UIView, CGRect) -> Void, externalDataUpdated: @escaping (ContainedViewLayoutTransition) -> Void ) { - let captureProtected = data.peer?.isCopyProtectionEnabled ?? false + var captureProtected = data.peer?.isCopyProtectionEnabled ?? false let paneNode: PeerInfoPaneNode switch key { - case .stories, .storyArchive: + case .stories, .storyArchive, .botPreview: var canManage = false if let peer = data.peer { if peer.id == context.account.peerId { @@ -419,7 +419,27 @@ private final class PeerInfoPendingPane { } } - let visualPaneNode = PeerInfoStoryPaneNode(context: context, scope: .peer(id: peerId, isSaved: false, isArchived: key == .storyArchive), captureProtected: captureProtected, isProfileEmbedded: true, canManageStories: canManage, navigationController: chatControllerInteraction.navigationController, listContext: key == .storyArchive ? data.storyArchiveListContext : data.storyListContext) + var listContext: StoryListContext? + var scope: PeerInfoStoryPaneNode.Scope = .peer(id: peerId, isSaved: false, isArchived: key == .storyArchive) + switch key { + case .storyArchive: + listContext = data.storyArchiveListContext + case .botPreview: + listContext = data.botPreviewStoryListContext + scope = .botPreview(id: peerId) + + if let peer = data.peer { + if let user = peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { + canManage = true + } + } + + captureProtected = false + default: + listContext = data.storyListContext + } + + let visualPaneNode = PeerInfoStoryPaneNode(context: context, scope: scope, captureProtected: captureProtected, isProfileEmbedded: true, canManageStories: canManage, navigationController: chatControllerInteraction.navigationController, listContext: listContext) paneNode = visualPaneNode visualPaneNode.openCurrentDate = { openMediaCalendar() @@ -790,7 +810,20 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat } for (_, pane) in self.pendingPanes { if let paneNode = pane.pane.node as? PeerInfoStoryPaneNode { - paneNode.updateSelectedStories(selectedStoryIds: selectedStoryIds, animated: animated) + paneNode.updateSelectedStories(selectedStoryIds: selectedStoryIds, animated: false) + } + } + } + + func updatePaneIsReordering(isReordering: Bool, animated: Bool) { + for (_, pane) in self.currentPanes { + if let paneNode = pane.node as? PeerInfoStoryPaneNode { + paneNode.updateIsReordering(isReordering: isReordering, animated: animated) + } + } + for (_, pane) in self.pendingPanes { + if let paneNode = pane.pane.node as? PeerInfoStoryPaneNode { + paneNode.updateIsReordering(isReordering: isReordering, animated: false) } } } @@ -1092,6 +1125,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat title = presentationData.strings.PeerInfo_PaneStories case .storyArchive: title = presentationData.strings.PeerInfo_PaneArchivedStories + case .botPreview: + title = presentationData.strings.PeerInfo_PaneBotPreviews case .media: title = presentationData.strings.PeerInfo_PaneMedia case .files: diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index bf73590b15a..9a58243f490 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -125,6 +125,7 @@ import PeerNameColorItem import PeerSelectionScreen import UIKitRuntimeUtils import OldChannelsController +import UrlHandling public enum PeerInfoAvatarEditingMode { case generic @@ -142,7 +143,7 @@ protocol PeerInfoScreenItem: AnyObject { class PeerInfoScreenItemNode: ASDisplayNode, AccessibilityFocusableNode { var bringToFrontForHighlight: (() -> Void)? - func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { preconditionFailure() } @@ -181,7 +182,7 @@ private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode { self.addSubnode(self.bottomSeparatorNode) } - func update(width: CGFloat, safeInsets: UIEdgeInsets, hasCorners: Bool, presentationData: PresentationData, items: [PeerInfoScreenItem], transition: ContainedViewLayoutTransition) -> CGFloat { + func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, hasCorners: Bool, presentationData: PresentationData, items: [PeerInfoScreenItem], transition: ContainedViewLayoutTransition) -> CGFloat { self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor self.topSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor @@ -233,7 +234,7 @@ private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode { bottomItem = items[i + 1] } - let itemHeight = itemNode.update(width: width, safeInsets: safeInsets, presentationData: presentationData, item: item, topItem: topItem, bottomItem: bottomItem, hasCorners: hasCorners, transition: itemTransition) + let itemHeight = itemNode.update(context: context, width: width, safeInsets: safeInsets, presentationData: presentationData, item: item, topItem: topItem, bottomItem: bottomItem, hasCorners: hasCorners, transition: itemTransition) let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight)) itemTransition.updateFrame(node: itemNode, frame: itemFrame) if wasAdded { @@ -590,7 +591,7 @@ private final class PeerInfoInteraction { let editingToggleMessageSignatures: (Bool) -> Void let openParticipantsSection: (PeerInfoParticipantsSection) -> Void let openRecentActions: () -> Void - let openStats: (Bool) -> Void + let openStats: (ChannelStatsSection) -> Void let editingOpenPreHistorySetup: () -> Void let editingOpenAutoremoveMesages: () -> Void let openPermissions: () -> Void @@ -662,7 +663,7 @@ private final class PeerInfoInteraction { editingToggleMessageSignatures: @escaping (Bool) -> Void, openParticipantsSection: @escaping (PeerInfoParticipantsSection) -> Void, openRecentActions: @escaping () -> Void, - openStats: @escaping (Bool) -> Void, + openStats: @escaping (ChannelStatsSection) -> Void, editingOpenPreHistorySetup: @escaping () -> Void, editingOpenAutoremoveMesages: @escaping () -> Void, openPermissions: @escaping () -> Void, @@ -1317,6 +1318,7 @@ private enum InfoSection: Int, CaseIterable { case calls case personalChannel case peerInfo + case peerInfoTrailing case peerMembers } @@ -1336,6 +1338,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese ) let bioIconAction = interaction.translate // + + var currentPeerInfoSection: InfoSection = .peerInfo var items: [InfoSection: [PeerInfoScreenItem]] = [:] for section in InfoSection.allCases { @@ -1395,12 +1399,12 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } else { label = presentationData.strings.ContactInfo_PhoneLabelMobile } - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 2, label: label, text: formattedPhone, textColor: .accent, action: { node, progress in + items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 2, label: label, text: formattedPhone, textColor: .accent, action: { node, progress in interaction.openPhone(phone, node, nil, progress) }, longTapAction: nil, contextAction: { node, gesture, _ in interaction.openPhone(phone, node, gesture, nil) - }, requestLayout: { - interaction.requestLayout(false) + }, requestLayout: { animated in + interaction.requestLayout(animated) })) } if let mainUsername = user.addressName { @@ -1410,7 +1414,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese additionalUsernames = presentationData.strings.Profile_AdditionalUsernames(String(usernames.map { "@\($0.username)" }.joined(separator: ", "))).string } - items[.peerInfo]!.append( + items[currentPeerInfoSection]!.append( PeerInfoScreenLabeledValueItem( id: 1, label: presentationData.strings.Profile_Username, @@ -1430,8 +1434,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese interaction.openQrCode() }, contextAction: { node, gesture, _ in interaction.openUsernameContextMenu(node, gesture) - }, requestLayout: { - interaction.requestLayout(false) + }, requestLayout: { animated in + interaction.requestLayout(animated) } ) ) @@ -1456,29 +1460,95 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } } - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 400, context: context, label: hasBirthdayToday ? presentationData.strings.UserInfo_BirthdayToday : presentationData.strings.UserInfo_Birthday, text: stringForCompactBirthday(birthday, strings: presentationData.strings, showAge: true), textColor: .primary, leftIcon: hasBirthdayToday ? .birthday : nil, icon: hasBirthdayToday ? .premiumGift : nil, action: birthdayAction, longTapAction: nil, iconAction: { + items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 400, context: context, label: hasBirthdayToday ? presentationData.strings.UserInfo_BirthdayToday : presentationData.strings.UserInfo_Birthday, text: stringForCompactBirthday(birthday, strings: presentationData.strings, showAge: true), textColor: .primary, leftIcon: hasBirthdayToday ? .birthday : nil, icon: hasBirthdayToday ? .premiumGift : nil, action: birthdayAction, longTapAction: nil, iconAction: { interaction.openPremiumGift() - }, contextAction: birthdayContextAction, requestLayout: { + }, contextAction: birthdayContextAction, requestLayout: { _ in })) } + var hasAbout = false + if let about = cachedData.about, !about.isEmpty { + hasAbout = true + } + + var hasWebApp = false + if let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) { + hasWebApp = true + } + if user.isFake { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: "", text: user.botInfo != nil ? presentationData.strings.UserInfo_FakeBotWarning : presentationData.strings.UserInfo_FakeUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: { - interaction.requestLayout(false) + items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: "", text: user.botInfo != nil ? presentationData.strings.UserInfo_FakeBotWarning : presentationData.strings.UserInfo_FakeUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: { animated in + interaction.requestLayout(animated) })) } else if user.isScam { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: { - interaction.requestLayout(false) + items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: { animated in + interaction.requestLayout(animated) })) - } else if let about = cachedData.about, !about.isEmpty { - // MARK: Nicegram TranslateBio, add icon and iconAction - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), icon: bioIcon, action: isMyProfile ? { node, _ in bioContextAction(node, nil, nil) } : nil, linkItemAction: bioLinkAction, iconAction: { bioIconAction(about) }, contextAction: bioContextAction, requestLayout: { - interaction.requestLayout(false) + } else if hasAbout || hasWebApp { + var actionButton: PeerInfoScreenLabeledValueItem.Button? + if hasWebApp { + actionButton = PeerInfoScreenLabeledValueItem.Button(title: presentationData.strings.PeerInfo_OpenAppButton, action: { + guard let parentController = interaction.getController() else { + return + } + + if let navigationController = parentController.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer { + for controller in minimizedContainer.controllers { + if let controller = controller as? AttachmentController, let mainController = controller.mainController as? WebAppController, mainController.botId == user.id && mainController.source == .generic { + navigationController.maximizeViewController(controller, animated: true) + return + } + } + } + + context.sharedContext.openWebApp( + context: context, + parentController: parentController, + updatedPresentationData: nil, + peer: .user(user), + threadId: nil, + buttonText: "", + url: "", + simple: true, + source: .generic, + skipTermsOfService: true + ) + }) + } + + var label: String = "" + if let about = cachedData.about, !about.isEmpty { + label = user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo + } + + // MARK: Nicegram TranslateBio, add icon and iconAction + items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: label, text: cachedData.about ?? "", textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), icon: bioIcon, action: isMyProfile ? { node, _ in + bioContextAction(node, nil, nil) + } : nil, linkItemAction: bioLinkAction, iconAction: { bioIconAction(cachedData.about ?? "") }, button: actionButton, contextAction: bioContextAction, requestLayout: { animated in + interaction.requestLayout(animated) })) + + if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { + items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: 800, text: presentationData.strings.PeerInfo_AppFooterAdmin, linkAction: { action in + if case let .tap(url) = action { + context.sharedContext.applicationBindings.openUrl(url) + } + })) + + currentPeerInfoSection = .peerInfoTrailing + } else if actionButton != nil { + items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: 800, text: presentationData.strings.PeerInfo_AppFooter, linkAction: { action in + if case let .tap(url) = action { + context.sharedContext.applicationBindings.openUrl(url) + } + })) + + currentPeerInfoSection = .peerInfoTrailing + } } if let businessHours = cachedData.businessHours { - items[.peerInfo]!.append(PeerInfoScreenBusinessHoursItem(id: 300, label: presentationData.strings.PeerInfo_BusinessHours_Label, businessHours: businessHours, requestLayout: { animated in + items[currentPeerInfoSection]!.append(PeerInfoScreenBusinessHoursItem(id: 300, label: presentationData.strings.PeerInfo_BusinessHours_Label, businessHours: businessHours, requestLayout: { animated in interaction.requestLayout(animated) }, longTapAction: nil, contextAction: workingHoursContextAction)) } @@ -1486,7 +1556,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if let businessLocation = cachedData.businessLocation { if let coordinates = businessLocation.coordinates { let imageSignal = chatMapSnapshotImage(engine: context.engine, resource: MapSnapshotMediaResource(latitude: coordinates.latitude, longitude: coordinates.longitude, width: 90, height: 90)) - items[.peerInfo]!.append(PeerInfoScreenAddressItem( + items[currentPeerInfoSection]!.append(PeerInfoScreenAddressItem( id: 301, label: presentationData.strings.PeerInfo_Location_Label, text: businessLocation.address, @@ -1497,7 +1567,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese contextAction: businessLocationContextAction )) } else { - items[.peerInfo]!.append(PeerInfoScreenAddressItem( + items[currentPeerInfoSection]!.append(PeerInfoScreenAddressItem( id: 301, label: presentationData.strings.PeerInfo_Location_Label, text: businessLocation.address, @@ -1511,25 +1581,25 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if !isMyProfile { if let reactionSourceMessageId = reactionSourceMessageId, !data.isContact { - items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.UserInfo_SendMessage, action: { + items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.UserInfo_SendMessage, action: { interaction.openChat(nil) })) - items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.ReportPeer_BanAndReport, color: .destructive, action: { + items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.ReportPeer_BanAndReport, color: .destructive, action: { interaction.openReport(.reaction(reactionSourceMessageId)) })) } else if let _ = nearbyPeerDistance { - items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.UserInfo_SendMessage, action: { + items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.UserInfo_SendMessage, action: { interaction.openChat(nil) })) - items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.ReportPeer_Report, color: .destructive, action: { + items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.ReportPeer_Report, color: .destructive, action: { interaction.openReport(.user) })) } else { if !data.isContact { if user.botInfo == nil { - items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.PeerInfo_AddToContacts, action: { + items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.PeerInfo_AddToContacts, action: { interaction.openAddContact() })) } @@ -1541,14 +1611,14 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } if isBlocked { - items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: user.botInfo != nil ? presentationData.strings.Bot_Unblock : presentationData.strings.Conversation_Unblock, action: { + items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: 4, text: user.botInfo != nil ? presentationData.strings.Bot_Unblock : presentationData.strings.Conversation_Unblock, action: { interaction.updateBlocked(false) })) } else { if user.flags.contains(.isSupport) || data.isContact { } else { if user.botInfo == nil { - items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.Conversation_BlockUser, color: .destructive, action: { + items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.Conversation_BlockUser, color: .destructive, action: { interaction.updateBlocked(true) })) } @@ -1556,22 +1626,47 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } if let encryptionKeyFingerprint = data.encryptionKeyFingerprint { - items[.peerInfo]!.append(PeerInfoScreenDisclosureEncryptionKeyItem(id: 5, text: presentationData.strings.Profile_EncryptionKey, fingerprint: encryptionKeyFingerprint, action: { + items[currentPeerInfoSection]!.append(PeerInfoScreenDisclosureEncryptionKeyItem(id: 5, text: presentationData.strings.Profile_EncryptionKey, fingerprint: encryptionKeyFingerprint, action: { interaction.openEncryptionKey() })) } + + let starsBalance = data.starsRevenueStatsState?.balances.currentBalance ?? 0 + let overallStarsBalance = data.starsRevenueStatsState?.balances.overallRevenue ?? 0 - if user.botInfo != nil { - items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 6, text: presentationData.strings.ReportPeer_Report, action: { + if overallStarsBalance > 0 { + var string = "" + if overallStarsBalance > 0 { + string.append("*\(presentationStringsFormattedNumber(Int32(starsBalance), presentationData.dateTimeFormat.groupingSeparator))") + } + let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor) + if let range = attributedString.string.range(of: "*") { + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) + } + + items[currentPeerInfoSection]!.append(PeerInfoScreenDisclosureItem(id: 9, label: .attributedText(attributedString), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.balance, action: { + interaction.editingOpenStars() + })) + } + + if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { + items[currentPeerInfoSection]!.append(PeerInfoScreenDisclosureItem(id: 10, label: .none, text: presentationData.strings.Bot_Settings, icon: UIImage(bundleImageName: "Chat/Info/SettingsIcon"), action: { + interaction.openEditing() + })) + } + + if let botInfo = user.botInfo, !botInfo.flags.contains(.canEdit) { + items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: 6, text: presentationData.strings.ReportPeer_Report, action: { interaction.openReport(.default) })) } if let botInfo = user.botInfo, botInfo.flags.contains(.worksWithGroups) { - items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 7, text: presentationData.strings.Bot_AddToChat, color: .accent, action: { + items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: 7, text: presentationData.strings.Bot_AddToChat, color: .accent, action: { interaction.openAddBotToGroup() })) - items[.peerInfo]!.append(PeerInfoScreenCommentItem(id: 8, text: presentationData.strings.Bot_AddToChatInfo)) + items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: 8, text: presentationData.strings.Bot_AddToChatInfo)) } } } @@ -1587,7 +1682,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese let ItemAdmins = 6 let ItemMembers = 7 let ItemMemberRequests = 8 - let ItemEdit = 9 + let ItemBalance = 9 + let ItemEdit = 10 if let _ = data.threadData { let mainUsername: String @@ -1604,7 +1700,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese let linkText = "https://t.me/\(mainUsername)/\(threadId)" - items[.peerInfo]!.append( + items[currentPeerInfoSection]!.append( PeerInfoScreenLabeledValueItem( id: ItemUsername, label: presentationData.strings.Channel_LinkItem, @@ -1623,15 +1719,15 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } }, iconAction: { interaction.openQrCode() - }, requestLayout: { - interaction.requestLayout(false) + }, requestLayout: { animated in + interaction.requestLayout(animated) } ) ) if let _ = channel.addressName { } else { - items[.peerInfo]!.append(PeerInfoScreenCommentItem(id: ItemUsernameInfo, text: presentationData.strings.PeerInfo_PrivateShareLinkInfo)) + items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: ItemUsernameInfo, text: presentationData.strings.PeerInfo_PrivateShareLinkInfo)) } } else { if let location = (data.cachedData as? CachedChannelData)?.peerGeoLocation { @@ -1656,7 +1752,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese additionalUsernames = presentationData.strings.Profile_AdditionalUsernames(String(usernames.map { "@\($0.username)" }.joined(separator: ", "))).string } - items[.peerInfo]!.append( + items[currentPeerInfoSection]!.append( PeerInfoScreenLabeledValueItem( id: ItemUsername, label: presentationData.strings.Channel_LinkItem, @@ -1680,8 +1776,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } }, iconAction: { interaction.openQrCode() - }, requestLayout: { - interaction.requestLayout(false) + }, requestLayout: { animated in + interaction.requestLayout(animated) } ) ) @@ -1712,10 +1808,10 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese enabledEntities = enabledPrivateBioEntities } // MARK: Nicegram TranslateBio, add icon and iconAction - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledEntities), icon: bioIcon, action: isMyProfile ? { node, _ in + items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledEntities), icon: bioIcon, action: isMyProfile ? { node, _ in bioContextAction(node, nil, nil) - } : nil, linkItemAction: bioLinkAction, iconAction: { bioIconAction(aboutText) }, contextAction: bioContextAction, requestLayout: { - interaction.requestLayout(true) + } : nil, linkItemAction: bioLinkAction, iconAction: { bioIconAction(aboutText) }, contextAction: bioContextAction, requestLayout: { animated in + interaction.requestLayout(animated) })) } @@ -1742,6 +1838,40 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese })) } + if cachedData.flags.contains(.canViewRevenue) || cachedData.flags.contains(.canViewStarsRevenue) { + let revenueBalance = data.revenueStatsState?.balances.currentBalance ?? 0 + let starsBalance = data.starsRevenueStatsState?.balances.currentBalance ?? 0 + + let overallRevenueBalance = data.revenueStatsState?.balances.overallRevenue ?? 0 + let overallStarsBalance = data.starsRevenueStatsState?.balances.overallRevenue ?? 0 + + if overallRevenueBalance > 0 || overallStarsBalance > 0 { + var string = "" + if overallRevenueBalance > 0 { + string.append("#\(formatTonAmountText(revenueBalance, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))") + } + if overallStarsBalance > 0 { + if !string.isEmpty { + string.append(" ") + } + string.append("*\(presentationStringsFormattedNumber(Int32(starsBalance), presentationData.dateTimeFormat.groupingSeparator))") + } + let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor) + if let range = attributedString.string.range(of: "#") { + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton), range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) + } + if let range = attributedString.string.range(of: "*") { + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) + } + + items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemBalance, label: .attributedText(attributedString), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.balance, action: { + interaction.openStats(.monetization) + })) + } + } + items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemEdit, label: .none, text: presentationData.strings.Channel_Info_Settings, icon: UIImage(bundleImageName: "Chat/Info/SettingsIcon"), action: { interaction.openEditing() })) @@ -1768,10 +1898,10 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if let aboutText = aboutText { // MARK: Nicegram TranslateBio, add icon and iconAction - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPrivateBioEntities), icon: bioIcon, action: isMyProfile ? { node, _ in + items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPrivateBioEntities), icon: bioIcon, action: isMyProfile ? { node, _ in bioContextAction(node, nil, nil) - } : nil, linkItemAction: bioLinkAction, iconAction: { bioIconAction(aboutText) }, contextAction: bioContextAction, requestLayout: { - interaction.requestLayout(true) + } : nil, linkItemAction: bioLinkAction, iconAction: { bioIconAction(aboutText) }, contextAction: bioContextAction, requestLayout: { animated in + interaction.requestLayout(animated) })) } } @@ -1829,8 +1959,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if NGSettings.showProfileId { items[.nicegram]!.append(PeerInfoScreenLabeledValueItem(id: ngItemId, label: "id", text: idText, textColor: .primary, action: nil, longTapAction: { sourceNode in interaction.openPeerInfoContextMenu(.ngId(idText), sourceNode, nil) - }, requestLayout: { - interaction.requestLayout(false) + }, requestLayout: { animated in + interaction.requestLayout(animated) })) ngItemId += 1 } @@ -1859,8 +1989,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if hasRegDate { interaction.openPeerInfoContextMenu(.regDate(regDateText), sourceNode, nil) } - }, requestLayout: { - interaction.requestLayout(false) + }, requestLayout: { animated in + interaction.requestLayout(animated) })) ngItemId += 1 } @@ -1898,7 +2028,6 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL let ItemInfo = 3 let ItemDelete = 4 let ItemUsername = 5 - let ItemStars = 6 let ItemIntro = 7 let ItemCommands = 8 @@ -1909,13 +2038,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text("@\(user.addressName ?? "")"), text: presentationData.strings.PeerInfo_Bot_Username, icon: PresentationResourcesSettings.bot, action: { interaction.editingOpenPublicLinkSetup() })) - - if let starsRevenueStats = data.starsRevenueStatsState, starsRevenueStats.balances.overallRevenue > 0 { - items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemStars, label: .text(presentationData.strings.PeerInfo_Bot_Balance_Stars(Int32(starsRevenueStats.balances.currentBalance))), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.stars, action: { - interaction.editingOpenStars() - })) - } - + items[.peerSettings]!.append(PeerInfoScreenActionItem(id: ItemIntro, text: presentationData.strings.PeerInfo_Bot_EditIntro, icon: UIImage(bundleImageName: "Peer Info/BotIntro"), action: { interaction.openPeerMention("botfather", .withBotStartPayload(ChatControllerInitialBotStart(payload: "\(user.addressName ?? "")-intro", behavior: .interactive))) })) @@ -2136,7 +2259,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL if let cachedData = data.cachedData as? CachedChannelData, cachedData.flags.contains(.canViewStats) { items[.peerAdditionalSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemStats, label: .none, text: presentationData.strings.Channel_Info_Stats, icon: UIImage(bundleImageName: "Chat/Info/StatsIcon"), action: { - interaction.openStats(false) + interaction.openStats(.stats) })) } @@ -2649,6 +2772,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro isEditing: false, selectedMessageIds: nil, selectedStoryIds: nil, + paneIsReordering: false, updatingAvatar: nil, updatingBio: nil, avatarUploadProgress: nil, @@ -2846,8 +2970,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro openRecentActions: { [weak self] in self?.openRecentActions() }, - openStats: { [weak self] boosts in - self?.openStats(boosts: boosts) + openStats: { [weak self] section in + self?.openStats(section: section) }, editingOpenPreHistorySetup: { [weak self] in self?.editingOpenPreHistorySetup() @@ -3079,7 +3203,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let currentPeerId = strongSelf.peerId - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: targetLocation, subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always, useExisting: false, purposefulAction: { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: targetLocation, subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always, useExisting: false, purposefulAction: { var viewControllers = navigationController.viewControllers var indexesToRemove = Set() var keptCurrentChatController = false @@ -3241,7 +3365,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let currentPeerId = strongSelf.peerId - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: targetLocation, subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always, useExisting: false, purposefulAction: { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: targetLocation, subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always, useExisting: false, purposefulAction: { var viewControllers = navigationController.viewControllers var indexesToRemove = Set() var keptCurrentChatController = false @@ -3368,7 +3492,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in - }, tapMessage: nil, clickThroughMessage: { + }, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { [weak self] ids, value in guard let strongSelf = self else { return @@ -3426,7 +3550,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } if let foundGalleryMessage = foundGalleryMessage { - strongSelf.context.sharedContext.openChatInstantPage(context: strongSelf.context, message: foundGalleryMessage, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController) + if let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, message: foundGalleryMessage, sourcePeerType: associatedData?.automaticDownloadPeerType) { + navigationController.pushViewController(controller) + } } }, openWallpaper: { _ in }, openTheme: { _ in @@ -3726,6 +3852,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let self else { return } + self.headerNode.navigationButtonContainer.performAction?(.postStory, nil, nil) } @@ -4319,13 +4446,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.chatInterfaceInteraction.selectionState = strongSelf.state.selectedMessageIds.flatMap { ChatInterfaceSelectionState(selectedIds: $0) } strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) case .selectionDone: - strongSelf.state = strongSelf.state.withSelectedMessageIds(nil).withSelectedStoryIds(nil) + strongSelf.state = strongSelf.state.withSelectedMessageIds(nil).withSelectedStoryIds(nil).withPaneIsReordering(false) if let (layout, navigationHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring), additive: false) } strongSelf.chatInterfaceInteraction.selectionState = strongSelf.state.selectedMessageIds.flatMap { ChatInterfaceSelectionState(selectedIds: $0) } strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) strongSelf.paneContainerNode.updateSelectedStoryIds(strongSelf.state.selectedStoryIds, animated: true) + strongSelf.paneContainerNode.updatePaneIsReordering(isReordering: strongSelf.state.paneIsReordering, animated: true) case .search, .searchWithTags, .standaloneSearch: strongSelf.activateSearch() case .more: @@ -5339,43 +5467,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private func openPeer(peerId: PeerId, navigation: ChatControllerInteractionNavigateToPeer) { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in - guard let self, let peer = peer else { - return - } - - switch navigation { - case .default: - if let navigationController = self.controller?.navigationController as? NavigationController { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .always)) - } - case let .chat(_, subject, peekData): - if let navigationController = self.controller?.navigationController as? NavigationController { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: subject, keepStack: .always, peekData: peekData)) - } - case .info: - if peer.restrictionText(platform: "ios", contentSettings: self.context.currentContentSettings.with { $0 }) == nil { - if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { - (self.controller?.navigationController as? NavigationController)?.pushViewController(infoController) - } - } - case let .withBotStartPayload(startPayload): - if let navigationController = self.controller?.navigationController as? NavigationController { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), botStart: startPayload)) - } - case let .withAttachBot(attachBotStart): - if let navigationController = self.controller?.navigationController as? NavigationController { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), attachBotStart: attachBotStart)) - } - case let .withBotApp(botAppStart): - if let navigationController = self.controller?.navigationController as? NavigationController { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), botAppStart: botAppStart)) - } - } - }) + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + PeerInfoScreenImpl.openPeer(context: self.context, peerId: peerId, navigation: navigation, navigationController: navigationController) } - + private func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default) { let disposable: MetaDisposable if let resolvePeerByNameDisposable = self.resolvePeerByNameDisposable { @@ -5514,17 +5611,55 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let self else { return } + let context = self.context + let peerId = self.peerId + let params = WebAppParameters(source: .settings, peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: bot.flags.contains(.hasSettings), fullSize: true) + + var openUrlImpl: ((String, Bool, @escaping () -> Void) -> Void)? + var presentImpl: ((ViewController, Any?) -> Void)? - let params = WebAppParameters(source: .settings, peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: bot.flags.contains(.hasSettings), fullSize: true) - let controller = standaloneWebAppController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, params: params, threadId: nil, openUrl: { [weak self] url, concealed, commit in - self?.openUrl(url: url, concealed: concealed, external: false, forceExternal: true, commit: commit) + let controller = standaloneWebAppController(context: context, updatedPresentationData: self.controller?.updatedPresentationData, params: params, threadId: nil, openUrl: { url, concealed, commit in + openUrlImpl?(url, concealed, commit) }, requestSwitchInline: { _, _, _ in }, getNavigationController: { [weak self] in - return self?.controller?.navigationController as? NavigationController + return (self?.controller?.navigationController as? NavigationController) ?? context.sharedContext.mainWindow?.viewController as? NavigationController }) controller.navigationPresentation = .flatModal self.controller?.push(controller) + openUrlImpl = { [weak self, weak controller] url, concealed, commit in + let _ = openUserGeneratedUrl(context: context, peerId: peerId, url: url, concealed: concealed, present: { [weak self] c in + self?.controller?.present(c, in: .window(.root)) + }, openResolved: { result in + var navigationController: NavigationController? + if let current = self?.controller?.navigationController as? NavigationController { + navigationController = current + } else if let current = controller?.navigationController as? NavigationController { + navigationController = current + } + context.sharedContext.openResolvedUrl(result, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + if let navigationController { + PeerInfoScreenImpl.openPeer(context: context, peerId: peer.id, navigation: navigation, navigationController: navigationController) + } + commit() + }, sendFile: nil, + sendSticker: nil, + sendEmoji: nil, + requestMessageActionUrlAuth: nil, + joinVoiceChat: { peerId, invite, call in + + }, + present: { c, a in + presentImpl?(c, a) + }, dismissInput: { + context.sharedContext.mainWindow?.viewController?.view.endEditing(false) + }, contentContext: nil, progress: nil, completion: nil) + }) + } + presentImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + if installed { Queue.mainQueue().after(0.3, { let text: String @@ -6363,7 +6498,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, action: { [weak self] _, f in f(.dismissWithoutContent) - self?.openStats() + self?.openStats(section: .stats) }))) } if cachedData.flags.contains(.translationHidden) { @@ -8099,7 +8234,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.controller?.push(PeerInfoStoryGridScreen(context: self.context, peerId: self.peerId, scope: .archive)) } - private func openStats(boosts: Bool = false, boostStatus: ChannelBoostStatus? = nil) { + private func openStats(section: ChannelStatsSection, boostStatus: ChannelBoostStatus? = nil) { guard let controller = self.controller, let data = self.data, let peer = data.peer else { return } @@ -8109,7 +8244,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let channel = peer as? TelegramChannel, case .group = channel.info { statsController = groupStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id) } else { - statsController = channelStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, section: boosts ? .boosts : .stats, boostStatus: boostStatus) + statsController = channelStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, section: section, boostStatus: boostStatus) } controller.push(statsController) } @@ -10001,95 +10136,164 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.controller?.push(controller) } - private func openPostStory(sourceFrame: CGRect?) { - self.postingAvailabilityDisposable?.dispose() - - let canPostStatus: Signal - #if DEBUG && false - canPostStatus = .single(StoriesUploadAvailability.premiumRequired) - #else - canPostStatus = self.context.engine.messages.checkStoriesUploadAvailability(target: .peer(self.peerId)) - #endif + private func openBotPreviewEditor(target: Stories.PendingTarget, source: Any, transitionIn: (UIView, CGRect, UIImage?)?) { + let context = self.context + + let externalState = MediaEditorTransitionOutExternalState( + storyTarget: target, + isForcedTarget: false, + isPeerArchived: false, + transitionOut: nil + ) - self.postingAvailabilityDisposable = (canPostStatus - |> deliverOnMainQueue).startStrict(next: { [weak self] status in - guard let self else { - return - } - switch status { - case .available: - var cameraTransitionIn: StoryCameraTransitionIn? - if let rightButton = self.headerNode.navigationButtonContainer.rightButtonNodes[.postStory] { - cameraTransitionIn = StoryCameraTransitionIn( - sourceView: rightButton.view, - sourceRect: rightButton.view.bounds, - sourceCornerRadius: rightButton.view.bounds.height * 0.5 - ) + let controller = context.sharedContext.makeBotPreviewEditorScreen( + context: context, + source: source, + target: target, + transitionArguments: transitionIn, + transitionOut: { [weak self] in + guard let self else { + return nil } - if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - let coordinator = rootController.openStoryCamera(customTarget: self.peerId, transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: self.storyCameraTransitionOut()) - coordinator?.animateIn() + if let pane = self.paneContainerNode.currentPane?.node as? PeerInfoStoryPaneNode, let transitionView = pane.extractPendingStoryTransitionView() { + return BotPreviewEditorTransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: 0.0, + completion: { [weak transitionView] in + transitionView?.removeFromSuperview() + } + ) } - case .channelBoostRequired: - self.postingAvailabilityDisposable?.dispose() - self.postingAvailabilityDisposable = combineLatest( - queue: Queue.mainQueue(), - self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId)), - self.context.engine.peers.getChannelBoostStatus(peerId: self.peerId), - self.context.engine.peers.getMyBoostStatus() - ).startStrict(next: { [weak self] peer, boostStatus, myBoostStatus in - guard let self, let peer, let boostStatus, let myBoostStatus else { + return nil + }, + externalState: externalState, + completion: { result, commit in + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + var viewControllers = rootController.viewControllers + viewControllers = viewControllers.filter { !($0 is AttachmentController)} + rootController.setViewControllers(viewControllers, animated: false) + + rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + } + }, + cancelled: {} + ) + self.controller?.push(controller) + } + + private func openPostStory(sourceFrame: CGRect?) { + self.postingAvailabilityDisposable?.dispose() + + if let data = self.data, let user = data.peer as? TelegramUser, let botInfo = user.botInfo { + if !botInfo.flags.contains(.canEdit) { + return + } + let controller = self.context.sharedContext.makeStoryMediaPickerScreen( + context: self.context, + isDark: false, + getSourceRect: { return .zero }, + completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut, dismissed in + guard let self else { return } - if let navigationController = self.controller?.navigationController as? NavigationController { - if let previousController = navigationController.viewControllers.last as? ShareWithPeersScreen { - previousController.dismiss() - } - let controller = self.context.sharedContext.makePremiumBoostLevelsController(context: self.context, peerId: peer.id, subject: .stories, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: { [weak self] in - if let self { - self.openStats(boosts: true, boostStatus: boostStatus) - } - }) - navigationController.pushViewController(controller) + guard let pane = self.paneContainerNode.currentPane?.node as? PeerInfoStoryPaneNode else { + return } - self.hapticFeedback.impact(.light) - }).strict() - case .premiumRequired, .monthlyLimit, .weeklyLimit, .expiringLimit: - if let sourceFrame { - let context = self.context - let location = CGRect(origin: CGPoint(x: sourceFrame.midX, y: sourceFrame.maxY), size: CGSize()) + self.openBotPreviewEditor(target: .botPreview(id: self.peerId, language: pane.currentBotPreviewLanguage?.id), source: result, transitionIn: (transitionView, transitionRect, transitionImage)) + }, + dismissed: {}, + groupsPresented: {} + ) + self.controller?.push(controller) + } else { + let canPostStatus: Signal + canPostStatus = self.context.engine.messages.checkStoriesUploadAvailability(target: .peer(self.peerId)) + + self.postingAvailabilityDisposable = (canPostStatus + |> deliverOnMainQueue).startStrict(next: { [weak self] status in + guard let self else { + return + } + switch status { + case .available: + var cameraTransitionIn: StoryCameraTransitionIn? + if let rightButton = self.headerNode.navigationButtonContainer.rightButtonNodes[.postStory] { + cameraTransitionIn = StoryCameraTransitionIn( + sourceView: rightButton.view, + sourceRect: rightButton.view.bounds, + sourceCornerRadius: rightButton.view.bounds.height * 0.5 + ) + } - let text: String - text = self.presentationData.strings.StoryFeed_TooltipPremiumPostingLimited + if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + let coordinator = rootController.openStoryCamera(customTarget: self.peerId == self.context.account.peerId ? nil : .peer(self.peerId), transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: self.storyCameraTransitionOut()) + coordinator?.animateIn() + } + case .channelBoostRequired: + self.postingAvailabilityDisposable?.dispose() - let tooltipController = TooltipScreen( - context: context, - account: context.account, - sharedContext: context.sharedContext, - text: .markdown(text: text), - style: .customBlur(UIColor(rgb: 0x2a2a2a), 2.0), - icon: .none, - location: .point(location, .top), - shouldDismissOnTouch: { [weak self] point, containerFrame in - if containerFrame.contains(point) { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories, forceDark: false, dismissed: nil) - self?.controller?.push(controller) - return .dismiss(consume: true) - } else { - return .dismiss(consume: false) + self.postingAvailabilityDisposable = combineLatest( + queue: Queue.mainQueue(), + self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId)), + self.context.engine.peers.getChannelBoostStatus(peerId: self.peerId), + self.context.engine.peers.getMyBoostStatus() + ).startStrict(next: { [weak self] peer, boostStatus, myBoostStatus in + guard let self, let peer, let boostStatus, let myBoostStatus else { + return + } + + if let navigationController = self.controller?.navigationController as? NavigationController { + if let previousController = navigationController.viewControllers.last as? ShareWithPeersScreen { + previousController.dismiss() } + let controller = self.context.sharedContext.makePremiumBoostLevelsController(context: self.context, peerId: peer.id, subject: .stories, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: { [weak self] in + if let self { + self.openStats(section: .boosts, boostStatus: boostStatus) + } + }) + navigationController.pushViewController(controller) } - ) - self.controller?.present(tooltipController, in: .current) + + self.hapticFeedback.impact(.light) + }).strict() + case .premiumRequired, .monthlyLimit, .weeklyLimit, .expiringLimit: + if let sourceFrame { + let context = self.context + let location = CGRect(origin: CGPoint(x: sourceFrame.midX, y: sourceFrame.maxY), size: CGSize()) + + let text: String + text = self.presentationData.strings.StoryFeed_TooltipPremiumPostingLimited + + let tooltipController = TooltipScreen( + context: context, + account: context.account, + sharedContext: context.sharedContext, + text: .markdown(text: text), + style: .customBlur(UIColor(rgb: 0x2a2a2a), 2.0), + icon: .none, + location: .point(location, .top), + shouldDismissOnTouch: { [weak self] point, containerFrame in + if containerFrame.contains(point) { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories, forceDark: false, dismissed: nil) + self?.controller?.push(controller) + return .dismiss(consume: true) + } else { + return .dismiss(consume: false) + } + } + ) + self.controller?.present(tooltipController, in: .current) + } + default: + break } - default: - break - } - }).strict() + }).strict() + } } private func storyCameraTransitionOut() -> (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut? { @@ -10098,16 +10302,31 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return nil } - if !self.headerNode.isAvatarExpanded { - let transitionView = self.headerNode.avatarListNode.avatarContainerNode.avatarNode.contentNode.view - return StoryCameraTransitionOut( - destinationView: transitionView, - destinationRect: transitionView.bounds, - destinationCornerRadius: transitionView.bounds.height * 0.5 - ) + if let data = self.data, let user = data.peer as? TelegramUser, let _ = user.botInfo { + if let pane = self.paneContainerNode.currentPane?.node as? PeerInfoStoryPaneNode, let transitionView = pane.extractPendingStoryTransitionView() { + return StoryCameraTransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: 0.0, + completion: { [weak transitionView] in + transitionView?.removeFromSuperview() + } + ) + } + + return nil + } else { + if !self.headerNode.isAvatarExpanded { + let transitionView = self.headerNode.avatarListNode.avatarContainerNode.avatarNode.contentNode.view + return StoryCameraTransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: transitionView.bounds.height * 0.5 + ) + } + + return nil } - - return nil } } @@ -11021,153 +11240,96 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func displayMediaGalleryContextMenu(source: ContextReferenceContentNode, gesture: ContextGesture?) { let peerId = self.peerId - let _ = (self.context.engine.data.get(EngineDataMap([ - TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: peerId, threadId: self.chatLocation.threadId, tag: .photo), - TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: peerId, threadId: self.chatLocation.threadId, tag: .video) - ])) - |> deliverOnMainQueue).startStandalone(next: { [weak self] messageCounts in - guard let strongSelf = self else { + if let currentPaneKey = self.paneContainerNode.currentPaneKey, case .botPreview = currentPaneKey { + guard let controller = self.controller else { return } - - var mediaCount: [MessageTags: Int32] = [:] - for (key, count) in messageCounts { - mediaCount[key.tag] = count.flatMap(Int32.init) ?? 0 - } - - let photoCount: Int32 = mediaCount[.photo] ?? 0 - let videoCount: Int32 = mediaCount[.video] ?? 0 - - guard let controller = strongSelf.controller else { + guard let pane = self.paneContainerNode.currentPane?.node as? PeerInfoStoryPaneNode else { return } - guard let pane = strongSelf.paneContainerNode.currentPane?.node as? PeerInfoVisualMediaPaneNode else { + guard let data = self.data, let user = data.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) else { return } - + var items: [ContextMenuItem] = [] - - let strings = strongSelf.presentationData.strings - - var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)? - let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in - let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement - let canZoom: Bool = nextZoomLevel != nil - - return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? strings.SharedMedia_ZoomIn : strings.SharedMedia_ZoomOut, textColor: canZoom ? .primary : .disabled, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)) - }, action: canZoom ? { action in - guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else { + + let strings = self.presentationData.strings + + var ignoreNextActions = false + + if pane.canAddMoreBotPreviews() { + items.append(.action(ContextMenuActionItem(text: strings.BotPreviews_MenuAddPreview, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + if ignoreNextActions { return } - pane.updateZoomLevel(level: zoomLevel) - if let recurseGenerateAction = recurseGenerateAction { - action.updateAction(0, recurseGenerateAction(true)) - action.updateAction(1, recurseGenerateAction(false)) + ignoreNextActions = true + a(.default) + + if let self { + self.headerNode.navigationButtonContainer.performAction?(.postStory, nil, nil) } - } : nil) - } - recurseGenerateAction = { isZoomIn in - return generateAction(isZoomIn) + }))) } - - items.append(.action(generateAction(true))) - items.append(.action(generateAction(false))) - - var ignoreNextActions = false - if strongSelf.chatLocation.threadId == nil { - items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowCalendar, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Calendar"), color: theme.contextMenu.primaryColor) - }, action: { _, a in + + if pane.canReorder() { + items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) + }, action: { [weak pane] _, a in if ignoreNextActions { return } ignoreNextActions = true a(.default) - self?.openMediaCalendar() + if let pane { + pane.beginReordering() + } }))) } - - if photoCount != 0 && videoCount != 0 { - items.append(.separator) - - let showPhotos: Bool - switch pane.contentType { - case .photo, .photoOrVideo: - showPhotos = true - default: - showPhotos = false + + items.append(.action(ContextMenuActionItem(text: "Select", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + if ignoreNextActions { + return } - let showVideos: Bool - switch pane.contentType { - case .video, .photoOrVideo: - showVideos = true - default: - showVideos = false + ignoreNextActions = true + a(.default) + + if let self { + self.toggleStorySelection(ids: [], isSelected: true) } - - items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowPhotos, icon: { theme in - if !showPhotos { - return nil - } - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + }))) + + if let language = pane.currentBotPreviewLanguage { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuDeleteLanguage(language.name).string, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak pane] _, a in - a(.default) - - guard let pane = pane else { + if ignoreNextActions { return } - let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType - switch pane.contentType { - case .photoOrVideo: - updatedContentType = .video - case .photo: - updatedContentType = .photo - case .video: - updatedContentType = .photoOrVideo - default: - updatedContentType = pane.contentType - } - pane.updateContentType(contentType: updatedContentType) - }))) - items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowVideos, icon: { theme in - if !showVideos { - return nil - } - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) - }, action: { [weak pane] _, a in + ignoreNextActions = true a(.default) - - guard let pane = pane else { - return - } - let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType - switch pane.contentType { - case .photoOrVideo: - updatedContentType = .photo - case .photo: - updatedContentType = .photoOrVideo - case .video: - updatedContentType = .video - default: - updatedContentType = pane.contentType + + if let pane { + pane.presentDeleteBotPreviewLanguage() } - pane.updateContentType(contentType: updatedContentType) }))) } - - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) - contextController.passthroughTouchEvent = { sourceView, point in + + let contextController = ContextController(presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + contextController.passthroughTouchEvent = { [weak self] sourceView, point in guard let strongSelf = self else { return .ignore } - + let localPoint = strongSelf.view.convert(sourceView.convert(point, to: nil), from: nil) guard let localResult = strongSelf.hitTest(localPoint, with: nil) else { return .dismiss(consume: true, result: nil) } - + var testView: UIView? = localResult while true { if let testViewValue = testView { @@ -11190,12 +11352,188 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro break } } - + return .dismiss(consume: true, result: nil) } - strongSelf.mediaGalleryContextMenu = contextController + self.mediaGalleryContextMenu = contextController controller.presentInGlobalOverlay(contextController) - }) + } else { + let _ = (self.context.engine.data.get(EngineDataMap([ + TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: peerId, threadId: self.chatLocation.threadId, tag: .photo), + TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: peerId, threadId: self.chatLocation.threadId, tag: .video) + ])) + |> deliverOnMainQueue).startStandalone(next: { [weak self] messageCounts in + guard let strongSelf = self else { + return + } + + var mediaCount: [MessageTags: Int32] = [:] + for (key, count) in messageCounts { + mediaCount[key.tag] = count.flatMap(Int32.init) ?? 0 + } + + let photoCount: Int32 = mediaCount[.photo] ?? 0 + let videoCount: Int32 = mediaCount[.video] ?? 0 + + guard let controller = strongSelf.controller else { + return + } + guard let pane = strongSelf.paneContainerNode.currentPane?.node as? PeerInfoVisualMediaPaneNode else { + return + } + + var items: [ContextMenuItem] = [] + + let strings = strongSelf.presentationData.strings + + var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)? + let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in + let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement + let canZoom: Bool = nextZoomLevel != nil + + return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? strings.SharedMedia_ZoomIn : strings.SharedMedia_ZoomOut, textColor: canZoom ? .primary : .disabled, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)) + }, action: canZoom ? { action in + guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else { + return + } + pane.updateZoomLevel(level: zoomLevel) + if let recurseGenerateAction = recurseGenerateAction { + action.updateAction(0, recurseGenerateAction(true)) + action.updateAction(1, recurseGenerateAction(false)) + } + } : nil) + } + recurseGenerateAction = { isZoomIn in + return generateAction(isZoomIn) + } + + items.append(.action(generateAction(true))) + items.append(.action(generateAction(false))) + + var ignoreNextActions = false + if strongSelf.chatLocation.threadId == nil { + items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowCalendar, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Calendar"), color: theme.contextMenu.primaryColor) + }, action: { _, a in + if ignoreNextActions { + return + } + ignoreNextActions = true + a(.default) + + self?.openMediaCalendar() + }))) + } + + if photoCount != 0 && videoCount != 0 { + items.append(.separator) + + let showPhotos: Bool + switch pane.contentType { + case .photo, .photoOrVideo: + showPhotos = true + default: + showPhotos = false + } + let showVideos: Bool + switch pane.contentType { + case .video, .photoOrVideo: + showVideos = true + default: + showVideos = false + } + + items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowPhotos, icon: { theme in + if !showPhotos { + return nil + } + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + }, action: { [weak pane] _, a in + a(.default) + + guard let pane = pane else { + return + } + let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType + switch pane.contentType { + case .photoOrVideo: + updatedContentType = .video + case .photo: + updatedContentType = .photo + case .video: + updatedContentType = .photoOrVideo + default: + updatedContentType = pane.contentType + } + pane.updateContentType(contentType: updatedContentType) + }))) + items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowVideos, icon: { theme in + if !showVideos { + return nil + } + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + }, action: { [weak pane] _, a in + a(.default) + + guard let pane = pane else { + return + } + let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType + switch pane.contentType { + case .photoOrVideo: + updatedContentType = .photo + case .photo: + updatedContentType = .photoOrVideo + case .video: + updatedContentType = .video + default: + updatedContentType = pane.contentType + } + pane.updateContentType(contentType: updatedContentType) + }))) + } + + let contextController = ContextController(presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + contextController.passthroughTouchEvent = { sourceView, point in + guard let strongSelf = self else { + return .ignore + } + + let localPoint = strongSelf.view.convert(sourceView.convert(point, to: nil), from: nil) + guard let localResult = strongSelf.hitTest(localPoint, with: nil) else { + return .dismiss(consume: true, result: nil) + } + + var testView: UIView? = localResult + while true { + if let testViewValue = testView { + if let node = testViewValue.asyncdisplaykit_node as? PeerInfoHeaderNavigationButton { + node.isUserInteractionEnabled = false + DispatchQueue.main.async { + node.isUserInteractionEnabled = true + } + return .dismiss(consume: false, result: nil) + } else if let node = testViewValue.asyncdisplaykit_node as? PeerInfoVisualMediaPaneNode { + node.brieflyDisableTouchActions() + return .dismiss(consume: false, result: nil) + } else if let node = testViewValue.asyncdisplaykit_node as? PeerInfoStoryPaneNode { + node.brieflyDisableTouchActions() + return .dismiss(consume: false, result: nil) + } else { + testView = testViewValue.superview + } + } else { + break + } + } + + return .dismiss(consume: true, result: nil) + } + strongSelf.mediaGalleryContextMenu = contextController + controller.presentInGlobalOverlay(contextController) + }) + } } private func openMediaCalendar() { @@ -11251,7 +11589,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro chatController: nil, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), - subject: .message(id: .id(index.id), highlight: nil, timecode: nil), + subject: .message(id: .id(index.id), highlight: nil, timecode: nil, setupReply: false), botStart: nil, updateTextInputState: nil, keepStack: .never, @@ -11272,7 +11610,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro )) }))) - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: .message(id: .id(index.id), highlight: nil, timecode: nil), botStart: nil, mode: .standard(.previewing), params: nil) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: .message(id: .id(index.id), highlight: nil, timecode: nil, setupReply: false), botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.controller?.presentInGlobalOverlay(contextController) @@ -11386,7 +11724,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.headerNode.customNavigationContentNode = self.paneContainerNode.currentPane?.node.navigationContentNode var isScrollEnabled = !self.isMediaOnly && self.headerNode.customNavigationContentNode == nil - if self.state.selectedStoryIds != nil { + if self.state.selectedStoryIds != nil || self.state.paneIsReordering { isScrollEnabled = false } if self.scrollNode.view.isScrollEnabled != isScrollEnabled { @@ -11475,7 +11813,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro contentHeight -= 16.0 } } - let sectionHeight = sectionNode.update(width: sectionWidth, safeInsets: UIEdgeInsets(), hasCorners: !insets.left.isZero, presentationData: self.presentationData, items: sectionItems, transition: transition) + let sectionHeight = sectionNode.update(context: self.context, width: sectionWidth, safeInsets: UIEdgeInsets(), hasCorners: !insets.left.isZero, presentationData: self.presentationData, items: sectionItems, transition: transition) let sectionFrame = CGRect(origin: CGPoint(x: insets.left, y: contentHeight), size: CGSize(width: sectionWidth, height: sectionHeight)) if additive { transition.updateFrameAdditive(node: sectionNode, frame: sectionFrame) @@ -11534,9 +11872,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.editingSections[sectionId] = sectionNode self.scrollNode.addSubnode(sectionNode) } - + let sectionWidth = layout.size.width - insets.left - insets.right - let sectionHeight = sectionNode.update(width: sectionWidth, safeInsets: UIEdgeInsets(), hasCorners: !insets.left.isZero, presentationData: self.presentationData, items: sectionItems, transition: transition) + let sectionHeight = sectionNode.update(context: self.context, width: sectionWidth, safeInsets: UIEdgeInsets(), hasCorners: !insets.left.isZero, presentationData: self.presentationData, items: sectionItems, transition: transition) let sectionFrame = CGRect(origin: CGPoint(x: insets.left, y: contentHeight), size: CGSize(width: sectionWidth, height: sectionHeight)) if wasAdded { @@ -11821,7 +12159,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } var disableTabSwitching = false - if self.state.selectedStoryIds != nil { + if self.state.selectedStoryIds != nil || self.state.paneIsReordering { disableTabSwitching = true } @@ -11852,7 +12190,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro rightNavigationButtons.insert(PeerInfoHeaderNavigationButtonSpec(key: .postStory, isForExpandedView: false), at: 0) } - if self.state.selectedMessageIds == nil && self.state.selectedStoryIds == nil { + if self.state.selectedMessageIds == nil && self.state.selectedStoryIds == nil && !self.state.paneIsReordering { if let currentPaneKey = self.paneContainerNode.currentPaneKey { switch currentPaneKey { case .files, .music, .links, .members: @@ -11872,6 +12210,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } case .media: rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .more, isForExpandedView: true)) + case .botPreview: + if let data = self.data, data.hasBotPreviewItems, let user = data.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { + rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .more, isForExpandedView: true)) + } default: break } @@ -12213,6 +12555,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.paneContainerNode.updateSelectedStoryIds(self.state.selectedStoryIds, animated: true) } + func togglePaneIsReordering(isReordering: Bool) { + self.expandTabs(animated: true) + + self.state = self.state.withPaneIsReordering(true) + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring), additive: false) + } + self.paneContainerNode.updatePaneIsReordering(isReordering: self.state.paneIsReordering, animated: true) + } + func cancelItemSelection() { self.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil) } @@ -12722,6 +13075,33 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.controllerNode.updateProfileVideo(image, asset: asset, adjustments: adjustments, mode: mode) } + static func openPeer(context: AccountContext, peerId: PeerId, navigation: ChatControllerInteractionNavigateToPeer, navigationController: NavigationController) { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).startStandalone(next: { peer in + guard let peer else { + return + } + switch navigation { + case .default: + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always)) + case let .chat(_, subject, peekData): + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: subject, keepStack: .always, peekData: peekData)) + case .info: + if peer.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) == nil { + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + navigationController.pushViewController(infoController) + } + } + case let .withBotStartPayload(startPayload): + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), botStart: startPayload)) + case let .withAttachBot(attachBotStart): + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), attachBotStart: attachBotStart)) + case let .withBotApp(botAppStart): + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), botAppStart: botAppStart)) + } + }) + } + public static func displayChatNavigationMenu(context: AccountContext, chatNavigationStack: [ChatNavigationStackItem], nextFolderId: Int32?, parentController: ViewController, backButtonView: UIView, navigationController: NavigationController, gesture: ContextGesture) { let peerMap = EngineDataMap( Set(chatNavigationStack.map(\.peerId)).map(TelegramEngine.EngineData.Item.Peer.Peer.init) @@ -13031,6 +13411,10 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.controllerNode.toggleStorySelection(ids: ids, isSelected: isSelected) } + public func togglePaneIsReordering(isReordering: Bool) { + self.controllerNode.togglePaneIsReordering(isReordering: isReordering) + } + public func cancelItemSelection() { self.controllerNode.cancelItemSelection() } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenMultilineInputtem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenMultilineInputtem.swift index 0e5affd07cc..9a639d6e133 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenMultilineInputtem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenMultilineInputtem.swift @@ -2,6 +2,7 @@ import AsyncDisplayKit import Display import TelegramPresentationData import ItemListUI +import AccountContext final class PeerInfoScreenMultilineInputItem: PeerInfoScreenItem { let id: AnyHashable @@ -53,7 +54,7 @@ final class PeerInfoScreenMultilineInputItemNode: PeerInfoScreenItemNode { self.addSubnode(self.bottomSeparatorNode) } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenMultilineInputItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD index a619f335403..0d10fc1c0f8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/SaveToCameraRoll", "//submodules/ShareController", "//submodules/OpenInExternalAppUI", + "//submodules/TelegramUI/Components/SaveProgressScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index e9fccb53a37..dbbf2ff7e33 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -14,7 +14,7 @@ import ChatTitleView import BottomButtonPanelComponent import UndoUI import MoreHeaderButton -import MediaEditorScreen +import SaveProgressScreen import SaveToCameraRoll final class PeerInfoStoryGridScreenComponent: Component { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift index 6bdc9c25811..3cf859f6635 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift @@ -14,7 +14,6 @@ import ChatTitleView import BottomButtonPanelComponent import UndoUI import MoreHeaderButton -import MediaEditorScreen import SaveToCameraRoll import ShareController import OpenInExternalAppUI diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD index 973f424075c..03dc3895ba4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD @@ -46,6 +46,8 @@ swift_library( "//submodules/TelegramUI/Components/MediaEditorScreen", "//submodules/LocationUI", "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/TabSelectorComponent", + "//submodules/TelegramUI/Components/Settings/LanguageSelectionScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index e254ffd8911..738ef1a489d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -41,6 +41,8 @@ import Geocoding import ItemListUI import MultilineTextComponent import LocationUI +import TabSelectorComponent +import LanguageSelectionScreen private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) private let mediaBadgeTextColor = UIColor.white @@ -92,12 +94,16 @@ private final class VisualMediaItem: SparseItemGrid.Item { override var index: Int { return self.indexValue } + override var isReorderable: Bool { + return self.isReorderableValue + } let localMonthTimestamp: Int32 let peer: PeerReference let storyId: StoryId let story: EngineStoryItem let authorPeer: EnginePeer? let isPinned: Bool + let isReorderableValue: Bool override var id: AnyHashable { return AnyHashable(self.storyId) @@ -111,7 +117,7 @@ private final class VisualMediaItem: SparseItemGrid.Item { return VisualMediaHoleAnchor(index: self.index, storyId: self.storyId, localMonthTimestamp: self.localMonthTimestamp) } - init(index: Int, peer: PeerReference, storyId: StoryId, story: EngineStoryItem, authorPeer: EnginePeer?, isPinned: Bool, localMonthTimestamp: Int32) { + init(index: Int, peer: PeerReference, storyId: StoryId, story: EngineStoryItem, authorPeer: EnginePeer?, isPinned: Bool, localMonthTimestamp: Int32, isReorderable: Bool) { self.indexValue = index self.peer = peer self.storyId = storyId @@ -119,6 +125,7 @@ private final class VisualMediaItem: SparseItemGrid.Item { self.authorPeer = authorPeer self.isPinned = isPinned self.localMonthTimestamp = localMonthTimestamp + self.isReorderableValue = isReorderable } } @@ -553,6 +560,10 @@ private final class ItemLayer: CALayer, SparseItemGridLayer { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override init(layer: Any) { + super.init(layer: layer) + } deinit { self.disposable?.dispose() @@ -1104,6 +1115,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { var onBeginFastScrollingImpl: (() -> Void)? var getShimmerColorsImpl: (() -> SparseItemGrid.ShimmerColors)? var updateShimmerLayersImpl: ((SparseItemGridDisplayItem) -> Void)? + var reorderIfPossibleImpl: ((SparseItemGrid.Item, Int) -> Void)? var revealedSpoilerMessageIds = Set() @@ -1356,6 +1368,12 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { return .never() } } + + func reorderIfPossible(item: SparseItemGrid.Item, toIndex: Int) { + if let reorderIfPossibleImpl = self.reorderIfPossibleImpl { + reorderIfPossibleImpl(item, toIndex) + } + } func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) { guard let item = item as? VisualMediaItem else { @@ -1476,6 +1494,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr case peer(id: EnginePeer.Id, isSaved: Bool, isArchived: Bool) case search(query: String) case location(coordinates: MediaArea.Coordinates, venue: MediaArea.Venue) + case botPreview(id: EnginePeer.Id) } public struct ZoomLevel { @@ -1548,11 +1567,17 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private var mapInfoNode: LocationInfoListItemNode? private var searchHeader: ComponentView? + private var botPreviewLanguageTab: ComponentView? + private var botPreviewFooter: ComponentView? + + private var barBackgroundLayer: SimpleLayer? + private let itemGrid: SparseItemGrid private let itemGridBinding: SparseItemGridBindingImpl private let directMediaImageCache: DirectMediaImageCache private var items: SparseItemGrid.Items? private var pinnedIds: Set = Set() + private var reorderedIds: [StoryId]? private var itemCount: Int? private var didUpdateItemsOnce: Bool = false @@ -1573,6 +1598,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return self.selectedIdsPromise.get() } + private var isReordering: Bool = false + public var selectedItems: [Int32: EngineStoryItem] { var result: [Int32: EngineStoryItem] = [:] for id in self.selectedIds { @@ -1602,6 +1629,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr public private(set) var isSelectionModeActive: Bool private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData)? + private var listBottomInset: CGFloat? private let ready = Promise() private var didSetReady: Bool = false @@ -1616,13 +1644,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? public var tabBarOffset: CGFloat { - return self.itemGrid.coveringInsetOffset + if case .botPreview = self.scope { + return 0.0 + } else { + return self.itemGrid.coveringInsetOffset + } } - + + private var currentListState: StoryListContext.State? private var listDisposable: Disposable? private var hiddenMediaDisposable: Disposable? private let updateDisposable = MetaDisposable() + private var currentBotPreviewLanguages: [StoryListContext.State.Language] = [] + private var removedBotPreviewLanguages = Set() + private var numberOfItemsToRequest: Int = 50 private var isRequestingView: Bool = false private var isFirstHistoryView: Bool = true @@ -1633,6 +1669,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr public private(set) var calendarSource: SparseMessageCalendar? private var listSource: StoryListContext + + private let maxBotPreviewCount: Int + + private let defaultListSource: StoryListContext + private var cachedListSources: [String: StoryListContext] = [:] + + public var currentBotPreviewLanguage: (id: String, name: String)? { + guard let listSource = self.listSource as? BotPreviewStoryListContext else { + return nil + } + guard let id = listSource.language else { + return nil + } + guard let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) else { + return nil + } + return (language.id, language.name) + } public var openCurrentDate: (() -> Void)? public var paneDidScroll: (() -> Void)? @@ -1699,10 +1753,19 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.listSource = SearchStoryListContext(account: context.account, source: .hashtag(query)) case let .location(coordinates, venue): self.listSource = SearchStoryListContext(account: context.account, source: .mediaArea(.venue(coordinates: coordinates, venue: venue))) + case let .botPreview(id): + self.listSource = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: id, language: nil, assumeEmpty: false) } } + self.defaultListSource = self.listSource self.calendarSource = nil + var maxBotPreviewCount = 10 + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["bot_preview_medias_max"] as? Double { + maxBotPreviewCount = Int(value) + } + self.maxBotPreviewCount = maxBotPreviewCount + super.init() if case .peer = self.scope { @@ -1863,6 +1926,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return nil } ) + storyContainerScreen.performReorderAction = { [weak self] in + guard let self else { + return + } + self.beginReordering() + } self.hiddenMediaDisposable?.dispose() self.hiddenMediaDisposable = (storyContainerScreen.focusedItem @@ -1908,6 +1977,13 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } strongSelf.openCurrentDate?() } + + self.itemGridBinding.reorderIfPossibleImpl = { [weak self] item, toIndex in + guard let self else { + return + } + self.reorderIfPossible(item: item, toIndex: toIndex) + } self.itemGridBinding.didScrollImpl = { [weak self] in guard let strongSelf = self else { @@ -1978,10 +2054,17 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return SparseItemGrid.ShimmerColors(background: 0xffffff, foreground: 0xffffff) } - let backgroundColor = presentationData.theme.list.mediaPlaceholderColor - let foregroundColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6) - - return SparseItemGrid.ShimmerColors(background: backgroundColor.argb, foreground: foregroundColor.argb) + if case .botPreview = scope { + let backgroundColor = presentationData.theme.list.plainBackgroundColor + let foregroundColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6) + + return SparseItemGrid.ShimmerColors(background: backgroundColor.argb, foreground: foregroundColor.argb) + } else { + let backgroundColor = presentationData.theme.list.mediaPlaceholderColor + let foregroundColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6) + + return SparseItemGrid.ShimmerColors(background: backgroundColor.argb, foreground: foregroundColor.argb) + } } self.itemGridBinding.updateShimmerLayersImpl = { [weak self] layer in @@ -2170,6 +2253,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr switch self.scope { case let .peer(_, _, isArchived): paneKey = isArchived ? .storyArchive : .stories + case .botPreview: + paneKey = .botPreview default: paneKey = .stories } @@ -2418,6 +2503,17 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: isPinned ? "anim_toastunpin" : "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) }))) + if isPinned && self.canReorder() { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + c?.dismiss(completion: { + guard let self else { + return + } + + self.beginReordering() + }) + }))) + } } items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Edit, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in @@ -2452,6 +2548,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr peer: peer, storyItem: item, videoPlaybackPosition: nil, + cover: false, repost: false, transitionIn: .gallery(MediaEditorScreen.TransitionIn.GalleryTransitionIn(sourceView: self.itemGrid.view, sourceRect: foundItemLayer?.frame ?? .zero, sourceImage: sourceImage)), transitionOut: MediaEditorScreen.TransitionOut(destinationView: self.itemGrid.view, destinationRect: foundItemLayer?.frame ?? .zero, destinationCornerRadius: 0.0), @@ -2470,6 +2567,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr }))) } + if canManage, case .botPreview = self.scope, self.canReorder() { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + c?.dismiss(completion: { + guard let self else { + return + } + + self.beginReordering() + }) + }))) + } + if !item.isForwardingDisabled, case .everyone = item.privacy?.base { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Forward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { @@ -2613,8 +2722,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.listDisposable?.dispose() self.listDisposable = nil + + if reloadAtTop { + self.didUpdateItemsOnce = false + } - let context = self.context self.listDisposable = (state |> deliverOn(queue)).startStrict(next: { [weak self] state in guard let self else { @@ -2623,7 +2735,15 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr let title: String if state.totalCount == 0 { - title = "" + if case .botPreview = self.scope { + if state.isLoading { + title = self.presentationData.strings.BotPreviews_SubtitleLoading + } else { + title = self.presentationData.strings.BotPreviews_SubtitleEmpty + } + } else { + title = "" + } } else if case let .peer(_, isSaved, isArchived) = self.scope { if isSaved { title = self.presentationData.strings.StoryList_SubtitleSaved(Int32(state.totalCount)) @@ -2632,6 +2752,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } else { title = self.presentationData.strings.StoryList_SubtitleCount(Int32(state.totalCount)) } + } else if case .botPreview = self.scope { + title = self.presentationData.strings.BotPreviews_SubtitleCount(Int32(state.totalCount)) } else { title = "" } @@ -2639,80 +2761,162 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr switch self.scope { case let .peer(_, _, isArchived): paneKey = isArchived ? .storyArchive : .stories + case .botPreview: + paneKey = .botPreview default: paneKey = .stories } self.statusPromise.set(.single(PeerInfoStatusData(text: title, isActivity: false, key: paneKey))) - - let timezoneOffset = Int32(TimeZone.current.secondsFromGMT()) - var mappedItems: [SparseItemGrid.Item] = [] - var mappedHoles: [SparseItemGrid.HoleAnchor] = [] - var totalCount: Int = 0 - for item in state.items { - var peerReference: PeerReference? - if let value = state.peerReference { - peerReference = value - } else if let peer = item.peer { - peerReference = PeerReference(peer._asPeer()) + Queue.mainQueue().async { [weak self] in + guard let self else { + return } - guard let peerReference else { - continue + + var botPreviewLanguages = self.currentBotPreviewLanguages + for language in state.availableLanguages { + if !botPreviewLanguages.contains(where: { $0.id == language.id }) && !self.removedBotPreviewLanguages.contains(language.id) { + botPreviewLanguages.append(language) + } } + botPreviewLanguages.sort(by: { $0.name < $1.name }) - mappedItems.append(VisualMediaItem( - index: mappedItems.count, - peer: peerReference, - storyId: item.id, - story: item.storyItem, - authorPeer: item.peer, - isPinned: state.pinnedIds.contains(item.storyItem.id), - localMonthTimestamp: Month(localTimestamp: item.storyItem.timestamp + timezoneOffset).packedValue - )) - } - if mappedItems.count < state.totalCount, let lastItem = state.items.last, let _ = state.loadMoreToken { - mappedHoles.append(VisualMediaHoleAnchor(index: mappedItems.count, storyId: StoryId(peerId: context.account.peerId, id: Int32.max), localMonthTimestamp: Month(localTimestamp: lastItem.storyItem.timestamp + timezoneOffset).packedValue)) - } - totalCount = state.totalCount - totalCount = max(mappedItems.count, totalCount) - - if totalCount == 0 && state.loadMoreToken != nil && !state.isCached { - totalCount = 100 - } - - Queue.mainQueue().async { [weak self] in - guard let strongSelf = self else { - return + var hadLocalItems = false + if let currentListState = self.currentListState { + for item in currentListState.items { + if item.storyItem.isPending { + hadLocalItems = true + } + } + } + + self.currentListState = state + + var hasLocalItems = false + if let currentListState = self.currentListState { + for item in currentListState.items { + if item.storyItem.isPending { + hasLocalItems = true + } + } } - var headerText: String? - if case let .peer(peerId, _, isArchived) = strongSelf.scope { - if isArchived && !mappedItems.isEmpty && peerId == strongSelf.context.account.peerId { - headerText = strongSelf.presentationData.strings.StoryList_ArchiveDescription + var synchronous = synchronous + if hasLocalItems != hadLocalItems { + synchronous = true + } + + self.updateItemsFromState(state: state, firstTime: firstTime, reloadAtTop: reloadAtTop, synchronous: synchronous, animated: false) + + if self.currentBotPreviewLanguages != botPreviewLanguages || reloadAtTop { + self.currentBotPreviewLanguages = botPreviewLanguages + if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: .immediate) } } - - let items = SparseItemGrid.Items( - items: mappedItems, - holeAnchors: mappedHoles, - count: totalCount, - itemBinding: strongSelf.itemGridBinding, - headerText: headerText, - snapTopInset: false - ) - strongSelf.itemCount = state.totalCount - - let currentSynchronous = synchronous && firstTime - let currentReloadAtTop = reloadAtTop && firstTime firstTime = false - strongSelf.updateHistory(items: items, pinnedIds: state.pinnedIds, synchronous: currentSynchronous, reloadAtTop: currentReloadAtTop) - strongSelf.isRequestingView = false + self.isRequestingView = false } }) } - private func updateHistory(items: SparseItemGrid.Items, pinnedIds: Set, synchronous: Bool, reloadAtTop: Bool) { + private func updateItemsFromState(state: StoryListContext.State, firstTime: Bool, reloadAtTop: Bool, synchronous: Bool, animated: Bool) { + let timezoneOffset = Int32(TimeZone.current.secondsFromGMT()) + + var mappedItems: [SparseItemGrid.Item] = [] + var mappedHoles: [SparseItemGrid.HoleAnchor] = [] + var totalCount: Int = 0 + + var stateItems = state.items + if let reorderedIds = self.reorderedIds { + var fixedStateItems: [StoryListContext.State.Item] = [] + + var seenIds = Set() + for id in reorderedIds { + if let index = stateItems.firstIndex(where: { $0.id == id }) { + seenIds.insert(id) + fixedStateItems.append(stateItems[index]) + } + } + + for item in stateItems { + if !seenIds.contains(item.id) { + fixedStateItems.append(item) + } + } + stateItems = fixedStateItems + self.reorderedIds = fixedStateItems.map(\.id) + } + + for item in stateItems { + var peerReference: PeerReference? + if let value = state.peerReference { + peerReference = value + } else if let peer = item.peer { + peerReference = PeerReference(peer._asPeer()) + } + guard let peerReference else { + continue + } + + var isReorderable = false + switch self.scope { + case .botPreview: + isReorderable = !item.storyItem.isPending + case let .peer(id, _, _): + if id == self.context.account.peerId { + isReorderable = state.pinnedIds.contains(item.storyItem.id) + } + default: + break + } + + mappedItems.append(VisualMediaItem( + index: mappedItems.count, + peer: peerReference, + storyId: item.id, + story: item.storyItem, + authorPeer: item.peer, + isPinned: state.pinnedIds.contains(item.storyItem.id), + localMonthTimestamp: Month(localTimestamp: item.storyItem.timestamp + timezoneOffset).packedValue, + isReorderable: isReorderable + )) + } + if mappedItems.count < state.totalCount, let lastItem = state.items.last, let _ = state.loadMoreToken { + mappedHoles.append(VisualMediaHoleAnchor(index: mappedItems.count, storyId: StoryId(peerId: context.account.peerId, id: Int32.max), localMonthTimestamp: Month(localTimestamp: lastItem.storyItem.timestamp + timezoneOffset).packedValue)) + } + totalCount = state.totalCount + totalCount = max(mappedItems.count, totalCount) + + if totalCount == 0 && state.loadMoreToken != nil && !state.isCached { + totalCount = 100 + } + + var headerText: String? + if case let .peer(peerId, _, isArchived) = self.scope { + if isArchived && !mappedItems.isEmpty && peerId == self.context.account.peerId { + headerText = self.presentationData.strings.StoryList_ArchiveDescription + } + } + + let items = SparseItemGrid.Items( + items: mappedItems, + holeAnchors: mappedHoles, + count: totalCount, + itemBinding: self.itemGridBinding, + headerText: headerText, + snapTopInset: false + ) + + self.itemCount = state.totalCount + + let currentSynchronous = synchronous && firstTime + let currentReloadAtTop = reloadAtTop && firstTime + self.updateHistory(items: items, pinnedIds: Set(state.pinnedIds), synchronous: currentSynchronous, reloadAtTop: currentReloadAtTop, animated: animated) + } + + private func updateHistory(items: SparseItemGrid.Items, pinnedIds: Set, synchronous: Bool, reloadAtTop: Bool, animated: Bool) { var transition: ContainedViewLayoutTransition = .immediate if case .location = self.scope, let previousItems = self.items, previousItems.items.count == 0, previousItems.count != 0, items.items.count == 0, items.count == 0 { transition = .animated(duration: 0.3, curve: .spring) @@ -2723,10 +2927,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { var gridSnapshot: UIView? - if reloadAtTop { + if case .botPreview = scope { + } else if reloadAtTop { gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false) } - self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: transition) + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: transition, animateGridItems: animated) self.updateSelectedItems(animated: false) if let gridSnapshot = gridSnapshot { self.view.addSubview(gridSnapshot) @@ -2744,6 +2949,58 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } } + private func reorderIfPossible(item: SparseItemGrid.Item, toIndex: Int) { + if let items = self.items, let item = item as? VisualMediaItem { + var toIndex = toIndex + if case .botPreview = self.scope { + } else if case let .peer(id, _, _) = self.scope { + if id == self.context.account.peerId { + let maxPinnedIndex = items.items.lastIndex(where: { ($0 as? VisualMediaItem)?.isPinned == true }) + if let maxPinnedIndex { + toIndex = min(toIndex, maxPinnedIndex) + } else { + return + } + } + } else { + return + } + + guard let toItem = items.items.first(where: { $0.index == toIndex }) as? VisualMediaItem else { + return + } + if item.story.isPending || toItem.story.isPending { + return + } + if !item.isReorderable { + return + } + + var ids = items.items.compactMap { item -> StoryId? in + return (item as? VisualMediaItem)?.storyId + } + + if let fromIndex = ids.firstIndex(of: item.storyId) { + if fromIndex < toIndex { + ids.insert(item.storyId, at: toIndex + 1) + ids.remove(at: fromIndex) + } else if fromIndex > toIndex { + ids.remove(at: fromIndex) + ids.insert(item.storyId, at: toIndex) + } + } + if self.reorderedIds != ids { + self.reorderedIds = ids + + HapticFeedback().tap() + + if let currentListState = self.currentListState { + self.updateItemsFromState(state: currentListState, firstTime: false, reloadAtTop: false, synchronous: false, animated: true) + } + } + } + } + public func scrollToTop() -> Bool { return self.itemGrid.scrollToTop() } @@ -2789,43 +3046,46 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil - - /*var foundItemLayer: SparseItemGridLayer? - self.itemGrid.forEachVisibleItem { item in - guard let itemLayer = item.layer as? ItemLayer else { - return - } - if let item = itemLayer.item, item.message.id == messageId { - foundItemLayer = itemLayer - } + } + + public func extractPendingStoryTransitionView() -> UIView? { + guard let items = self.items else { + return nil } - if let itemLayer = foundItemLayer { - let itemFrame = self.view.convert(self.itemGrid.frameForItem(layer: itemLayer), from: self.itemGrid.view) - let proxyNode = ASDisplayNode() - proxyNode.frame = itemFrame - if let contents = itemLayer.getContents() { - if let image = contents as? UIImage { - proxyNode.contents = image.cgImage - } else { - proxyNode.contents = contents - } + guard let visualItem = items.items.last(where: { item in + guard let item = item as? VisualMediaItem else { + return false } - proxyNode.isHidden = true - self.addSubnode(proxyNode) - - let escapeNotification = EscapeNotification { - proxyNode.removeFromSupernode() + if item.story.isPending { + return true } - - return (proxyNode, proxyNode.bounds, { - let view = UIView() - view.frame = proxyNode.frame - view.layer.contents = proxyNode.layer.contents - escapeNotification.keep() - return (view, nil) - }) + return false + }) else { + return nil } - return nil*/ + guard let item = self.itemGrid.item(at: visualItem.index) else { + return nil + } + + guard let itemLayer = item.layer as? ItemLayer else { + return nil + } + guard let story = itemLayer.item?.story else { + return nil + } + let rect = self.itemGrid.frameForItem(layer: itemLayer) + + let tempSourceNode = TempExtractedItemNode( + item: story, + itemLayer: itemLayer + ) + tempSourceNode.frame = rect + tempSourceNode.update(size: rect.size) + + self.tempContextContentItemNode = tempSourceNode + self.view.addSubview(tempSourceNode.view) + + return tempSourceNode.view } public func addToTransitionSurface(view: UIView) { @@ -2961,7 +3221,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } private func updateSelectedItems(animated: Bool) { - self.contextGestureContainerNode.isGestureEnabled = self.isProfileEmbedded && self.itemInteraction.selectedIds == nil + self.contextGestureContainerNode.isGestureEnabled = self.isProfileEmbedded && self.itemInteraction.selectedIds == nil && !self.isReordering self.itemGrid.forEachVisibleItem { item in guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else { @@ -3036,38 +3296,85 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } private func presentDeleteConfirmation(ids: Set) { - guard case let .peer(peerId, _, _) = self.scope else { - return - } - - let presentationData = self.presentationData - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - - let title: String = presentationData.strings.StoryList_DeleteConfirmation_Title(Int32(ids.count)) - - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: title), - ActionSheetButtonItem(title: presentationData.strings.StoryList_DeleteConfirmation_Action, color: .destructive, action: { [weak self] in - dismissAction() - - guard let self else { - return + if case let .peer(peerId, _, _) = self.scope { + let presentationData = self.presentationData + let controller = ActionSheetController(presentationData: presentationData) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + + let title: String = presentationData.strings.StoryList_DeleteConfirmation_Title(Int32(ids.count)) + + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: title), + ActionSheetButtonItem(title: presentationData.strings.StoryList_DeleteConfirmation_Action, color: .destructive, action: { [weak self] in + dismissAction() + + guard let self else { + return + } + + if let parentController = self.parentController as? PeerInfoScreen { + parentController.cancelItemSelection() + } + + let _ = self.context.engine.messages.deleteStories(peerId: peerId, ids: Array(ids)).startStandalone() + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + self.parentController?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } else if case let .botPreview(peerId) = self.scope { + let presentationData = self.presentationData + let controller = ActionSheetController(presentationData: presentationData) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + + var mappedMedia: [Media] = [] + if let items = self.items { + mappedMedia = items.items.compactMap { item -> Media? in + guard let item = item as? VisualMediaItem else { + return nil } - - if let parentController = self.parentController as? PeerInfoScreen { - parentController.cancelItemSelection() + if ids.contains(item.story.id) { + return item.story.media._asMedia() + } else { + return nil } - - let _ = self.context.engine.messages.deleteStories(peerId: peerId, ids: Array(ids)).startStandalone() - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - self.parentController?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + } + if mappedMedia.isEmpty { + return + } + + let title: String = presentationData.strings.BotPreviews_SheetDeleteTitle(Int32(mappedMedia.count)) + + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: title), + ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { [weak self] in + dismissAction() + + guard let self else { + return + } + guard let listSource = self.listSource as? BotPreviewStoryListContext else { + return + } + + if let parentController = self.parentController as? PeerInfoScreen { + parentController.cancelItemSelection() + } + + let _ = self.context.engine.messages.deleteBotPreviews(peerId: peerId, language: listSource.language, media: mappedMedia).startStandalone() + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + self.parentController?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } } private func update(transition: ContainedViewLayoutTransition) { @@ -3077,8 +3384,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } private func gridScrollingOffsetUpdated(transition: ContainedViewLayoutTransition) { - if let _ = self.mapNode, let currentParams = self.currentParams { - self.updateMapLayout(size: currentParams.size, topInset: currentParams.topInset, bottomInset: currentParams.bottomInset, deviceMetrics: currentParams.deviceMetrics, transition: transition) + if let currentParams = self.currentParams { + if let _ = self.mapNode { + self.updateMapLayout(size: currentParams.size, topInset: currentParams.topInset, bottomInset: currentParams.bottomInset, deviceMetrics: currentParams.deviceMetrics, transition: transition) + } + if case .botPreview = self.scope, self.canManageStories { + self.updateBotPreviewLanguageTab(size: currentParams.size, topInset: currentParams.topInset, transition: transition) + self.updateBotPreviewFooter(size: currentParams.size, bottomInset: 0.0, transition: transition) + } } } @@ -3232,7 +3545,163 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } } + private func updateBotPreviewLanguageTab(size: CGSize, topInset: CGFloat, transition: ContainedViewLayoutTransition) { + guard case .botPreview = self.scope, self.canManageStories else { + return + } + + let botPreviewLanguageTab: ComponentView + if let current = self.botPreviewLanguageTab { + botPreviewLanguageTab = current + } else { + botPreviewLanguageTab = ComponentView() + self.botPreviewLanguageTab = botPreviewLanguageTab + } + + var languageItems: [TabSelectorComponent.Item] = [] + languageItems.append(TabSelectorComponent.Item( + id: AnyHashable("_main"), + title: self.presentationData.strings.BotPreviews_LanguageTab_Main + )) + for language in self.currentBotPreviewLanguages { + languageItems.append(TabSelectorComponent.Item( + id: AnyHashable(language.id), + title: language.name + )) + } + languageItems.append(TabSelectorComponent.Item( + id: AnyHashable("_add"), + title: self.presentationData.strings.BotPreviews_LanguageTab_Add + )) + var selectedLanguageId = "_main" + if let listSource = self.listSource as? BotPreviewStoryListContext, let language = listSource.language { + selectedLanguageId = language + } + + let botPreviewLanguageTabSize = botPreviewLanguageTab.update( + transition: ComponentTransition(transition), + component: AnyComponent(TabSelectorComponent( + colors: TabSelectorComponent.Colors( + foreground: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.8), + selection: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05) + ), + customLayout: TabSelectorComponent.CustomLayout( + font: Font.medium(14.0), + spacing: 9.0, + verticalInset: 11.0 + ), + items: languageItems, + selectedId: AnyHashable(selectedLanguageId), + setSelectedId: { [weak self] id in + guard let self, let id = id.base as? String else { + return + } + if id == "_add" { + self.presentAddBotPreviewLanguage() + } else if id == "_main" { + self.setBotPreviewLanguage(id: nil, assumeEmpty: false) + } else if let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) { + self.setBotPreviewLanguage(id: language.id, assumeEmpty: false) + } + } + )), + environment: {}, + containerSize: CGSize(width: size.width, height: 44.0) + ) + var botPreviewLanguageTabFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewLanguageTabSize.width) * 0.5), y: topInset - 11.0), size: botPreviewLanguageTabSize) + + let effectiveScrollingOffset: CGFloat + effectiveScrollingOffset = self.itemGrid.scrollingOffset + botPreviewLanguageTabFrame.origin.y -= effectiveScrollingOffset + + let isSelectingOrReordering = self.isReordering || self.itemInteraction.selectedIds != nil + + if let botPreviewLanguageTabView = botPreviewLanguageTab.view { + if botPreviewLanguageTabView.superview == nil { + self.view.addSubview(botPreviewLanguageTabView) + } + transition.updateFrame(view: botPreviewLanguageTabView, frame: botPreviewLanguageTabFrame) + transition.updateAlpha(layer: botPreviewLanguageTabView.layer, alpha: isSelectingOrReordering ? 0.5 : 1.0) + botPreviewLanguageTabView.isUserInteractionEnabled = !isSelectingOrReordering + } + } + + private func updateBotPreviewFooter(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + if let items = self.items, !items.items.isEmpty { + var botPreviewFooterTransition = ComponentTransition(transition) + let botPreviewFooter: ComponentView + if let current = self.botPreviewFooter { + botPreviewFooter = current + } else { + botPreviewFooterTransition = .immediate + botPreviewFooter = ComponentView() + self.botPreviewFooter = botPreviewFooter + } + + var isMainLanguage = true + let text: String + if let listSource = self.listSource as? BotPreviewStoryListContext, let id = listSource.language, let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) { + isMainLanguage = false + + text = self.presentationData.strings.BotPreviews_TranslationFooter_Text(language.name).string + } else { + text = self.presentationData.strings.BotPreviews_DefaultFooter_Text + } + + let botPreviewFooterSize = botPreviewFooter.update( + transition: botPreviewFooterTransition, + component: AnyComponent(EmptyStateIndicatorComponent( + context: self.context, + theme: self.presentationData.theme, + fitToHeight: true, + animationName: nil, + title: nil, + text: text, + actionTitle: self.presentationData.strings.BotPreviews_Empty_Add, + action: { [weak self] in + guard let self else { + return + } + if self.canAddMoreBotPreviews() { + self.emptyAction?() + } else { + self.presentUnableToAddMorePreviewsAlert() + } + }, + additionalActionTitle: isMainLanguage ? self.presentationData.strings.BotPreviews_Empty_AddTranslation : nil, + additionalAction: { [weak self] in + guard let self else { + return + } + if isMainLanguage { + self.presentAddBotPreviewLanguage() + } + }, + additionalActionSeparator: isMainLanguage ? self.presentationData.strings.BotPreviews_Empty_Separator : nil + )), + environment: {}, + containerSize: CGSize(width: size.width, height: 1000.0) + ) + let botPreviewFooterFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewFooterSize.width) * 0.5), y: self.itemGrid.contentBottomOffset + 16.0), size: botPreviewFooterSize) + if let botPreviewFooterView = botPreviewFooter.view { + if botPreviewFooterView.superview == nil { + self.view.addSubview(botPreviewFooterView) + } + botPreviewFooterTransition.setFrame(view: botPreviewFooterView, frame: botPreviewFooterFrame) + } + } else { + if let botPreviewFooter = self.botPreviewFooter { + self.botPreviewFooter = nil + botPreviewFooter.view?.removeFromSuperview() + } + } + } + public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: transition, animateGridItems: false) + } + + private func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition, animateGridItems: Bool) { self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) var gridTopInset = topInset @@ -3266,7 +3735,32 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr mapOptionsNode.updateLayout(size: mapOptionsFrame.size, leftInset: sideInset, rightInset: sideInset, transition: transition) } + if self.isProfileEmbedded, case .botPreview = self.scope { + let barBackgroundLayer: SimpleLayer + if let current = self.barBackgroundLayer { + barBackgroundLayer = current + } else { + barBackgroundLayer = SimpleLayer() + self.barBackgroundLayer = barBackgroundLayer + self.layer.insertSublayer(barBackgroundLayer, at: 0) + } + barBackgroundLayer.backgroundColor = presentationData.theme.list.plainBackgroundColor.cgColor + transition.updateFrame(layer: barBackgroundLayer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: gridTopInset))) + } + + var listBottomInset = bottomInset var bottomInset = bottomInset + + if case .botPreview = self.scope, self.canManageStories { + updateBotPreviewLanguageTab(size: size, topInset: topInset, transition: transition) + gridTopInset += 50.0 + + updateBotPreviewFooter(size: size, bottomInset: 0.0, transition: transition) + if let botPreviewFooterView = self.botPreviewFooter?.view { + listBottomInset += 18.0 + botPreviewFooterView.bounds.height + } + } + if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case let .peer(peerId, _, isArchived) = self.scope { let selectionPanel: ComponentView var selectionPanelTransition = ComponentTransition(transition) @@ -3394,6 +3888,54 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr selectionPanelTransition.setFrame(view: selectionPanelView, frame: selectionPanelFrame) } bottomInset = selectionPanelSize.height + listBottomInset += selectionPanelSize.height + } else if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case .botPreview = self.scope { + let selectionPanel: ComponentView + var selectionPanelTransition = ComponentTransition(transition) + if let current = self.selectionPanel { + selectionPanel = current + } else { + selectionPanelTransition = selectionPanelTransition.withAnimation(.none) + selectionPanel = ComponentView() + self.selectionPanel = selectionPanel + } + + var selectionItems: [BottomActionsPanelComponent.Item] = [] + + selectionItems.append(BottomActionsPanelComponent.Item( + id: "delete", + color: .destructive, + title: presentationData.strings.StoryList_ActionPanel_Delete, + isEnabled: !selectedIds.isEmpty, + action: { [weak self] in + guard let self, let selectedIds = self.itemInteraction.selectedIds else { + return + } + + self.presentDeleteConfirmation(ids: selectedIds) + } + )) + + let selectionPanelSize = selectionPanel.update( + transition: selectionPanelTransition, + component: AnyComponent(BottomActionsPanelComponent( + theme: presentationData.theme, + insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), + items: selectionItems + )), + environment: {}, + containerSize: size + ) + let selectionPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - selectionPanelSize.height), size: selectionPanelSize) + if let selectionPanelView = selectionPanel.view { + if selectionPanelView.superview == nil { + self.view.addSubview(selectionPanelView) + transition.animatePositionAdditive(layer: selectionPanelView.layer, offset: CGPoint(x: 0.0, y: selectionPanelFrame.height)) + } + selectionPanelTransition.setFrame(view: selectionPanelView, frame: selectionPanelFrame) + } + bottomInset = selectionPanelSize.height + listBottomInset += selectionPanelSize.height } else if let selectionPanel = self.selectionPanel { self.selectionPanel = nil if let selectionPanelView = selectionPanel.view { @@ -3465,6 +4007,87 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr backgroundColor = presentationData.theme.list.blocksBackgroundColor } + if self.didUpdateItemsOnce { + ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)).setBackgroundColor(view: self.view, color: backgroundColor) + } else { + self.view.backgroundColor = backgroundColor + } + } else if case .botPreview = self.scope, let items = self.items, items.items.isEmpty, items.count == 0 { + let emptyStateView: ComponentView + var emptyStateTransition = ComponentTransition(transition) + if let current = self.emptyStateView { + emptyStateView = current + } else { + emptyStateTransition = .immediate + emptyStateView = ComponentView() + self.emptyStateView = emptyStateView + } + + var isMainLanguage = true + if let listSource = self.listSource as? BotPreviewStoryListContext, let _ = listSource.language { + isMainLanguage = false + } + + let emptyStateSize = emptyStateView.update( + transition: emptyStateTransition, + component: AnyComponent(EmptyStateIndicatorComponent( + context: self.context, + theme: presentationData.theme, + fitToHeight: self.isProfileEmbedded, + animationName: nil, + title: presentationData.strings.BotPreviews_Empty_Title, + text: presentationData.strings.BotPreviews_Empty_Text(Int32(self.maxBotPreviewCount)), + actionTitle: self.canManageStories ? presentationData.strings.BotPreviews_Empty_Add : nil, + action: { [weak self] in + guard let self else { + return + } + if self.canAddMoreBotPreviews() { + self.emptyAction?() + } else { + self.presentUnableToAddMorePreviewsAlert() + } + }, + additionalActionTitle: self.canManageStories ? (isMainLanguage ? presentationData.strings.BotPreviews_Empty_AddTranslation : presentationData.strings.BotPreviews_Empty_DeleteTranslation) : nil, + additionalAction: { + if isMainLanguage { + self.presentAddBotPreviewLanguage() + } else { + self.presentDeleteBotPreviewLanguage() + } + }, + additionalActionSeparator: self.canManageStories ? presentationData.strings.BotPreviews_Empty_Separator : nil + )), + environment: {}, + containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset) + ) + + let emptyStateFrame: CGRect + if self.isProfileEmbedded { + emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: max(gridTopInset + 22.0, floor((visibleHeight - gridTopInset - bottomInset - emptyStateSize.height) * 0.5))), size: emptyStateSize) + } else { + emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: gridTopInset), size: emptyStateSize) + } + + if let emptyStateComponentView = emptyStateView.view { + if emptyStateComponentView.superview == nil { + self.view.addSubview(emptyStateComponentView) + if self.didUpdateItemsOnce { + emptyStateComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + emptyStateTransition.setFrame(view: emptyStateComponentView, frame: emptyStateFrame) + } + + let backgroundColor: UIColor + if self.isProfileEmbedded, case .botPreview = self.scope { + backgroundColor = presentationData.theme.list.blocksBackgroundColor + } else if self.isProfileEmbedded { + backgroundColor = presentationData.theme.list.blocksBackgroundColor + } else { + backgroundColor = presentationData.theme.list.blocksBackgroundColor + } + if self.didUpdateItemsOnce { ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)).setBackgroundColor(view: self.view, color: backgroundColor) } else { @@ -3476,18 +4099,28 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.emptyStateView = nil if let emptyStateComponentView = emptyStateView.view { - subTransition.setAlpha(view: emptyStateComponentView, alpha: 0.0, completion: { [weak emptyStateComponentView] _ in - emptyStateComponentView?.removeFromSuperview() - }) + if self.didUpdateItemsOnce { + subTransition.setAlpha(view: emptyStateComponentView, alpha: 0.0, completion: { [weak emptyStateComponentView] _ in + emptyStateComponentView?.removeFromSuperview() + }) + } else { + emptyStateComponentView.removeFromSuperview() + } } - if self.isProfileEmbedded { + if self.isProfileEmbedded, case .botPreview = self.scope { + subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor) + } else if self.isProfileEmbedded { subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.plainBackgroundColor) } else { subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor) } } else { - self.view.backgroundColor = .clear + if self.isProfileEmbedded, case .botPreview = self.scope { + self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor + } else { + self.view.backgroundColor = .clear + } } } @@ -3500,9 +4133,19 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr fixedItemHeight = nil let fixedItemAspect: CGFloat? = 0.81 + + var adjustForSmallCount = true + if case .botPreview = self.scope { + adjustForSmallCount = false + } - self.itemGrid.pinchEnabled = items.count > 2 - self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none) + self.itemGrid.pinchEnabled = items.count > 2 && !self.isReordering + self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: listBottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, adjustForSmallCount: adjustForSmallCount, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none, transition: animateGridItems ? .spring(duration: 0.35) : .immediate) + } + + self.listBottomInset = listBottomInset + if case .botPreview = self.scope, self.canManageStories { + updateBotPreviewFooter(size: size, bottomInset: 0.0, transition: transition) } } @@ -3586,6 +4229,190 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr let levels = self.itemGrid.availableZoomLevels() return (levels.decrement.flatMap(ZoomLevel.init), levels.increment.flatMap(ZoomLevel.init)) } + + public func canAddMoreBotPreviews() -> Bool { + guard let items = self.items else { + return false + } + + return items.count < self.maxBotPreviewCount + } + + public func canReorder() -> Bool { + guard let items = self.items else { + return false + } + return items.count > 1 + } + + private func presentAddBotPreviewLanguage() { + let excludeIds: [String] = self.currentBotPreviewLanguages.map(\.id) + self.parentController?.push(LanguageSelectionScreen(context: self.context, excludeIds: excludeIds, selectLocalization: { [weak self] info in + guard let self else { + return + } + self.addBotPreviewLanguage(language: StoryListContext.State.Language(id: info.languageCode, name: info.title)) + })) + } + + public func presentUnableToAddMorePreviewsAlert() { + self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.BotPreviews_AlertTooManyPreviews(Int32(self.maxBotPreviewCount)), actions: [ + TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { + }) + ], parseMarkdown: true), in: .window(.root)) + } + + public func presentDeleteBotPreviewLanguage() { + self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Title, text: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Text, actions: [ + TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in + guard let self else { + return + } + if let listSource = self.listSource as? BotPreviewStoryListContext, let language = listSource.language { + self.deleteBotPreviewLanguage(id: language) + } + }) + ], parseMarkdown: true), in: .window(.root)) + } + + private func addBotPreviewLanguage(language: StoryListContext.State.Language) { + var botPreviewLanguages = self.currentBotPreviewLanguages + + var assumeEmpty = false + if !botPreviewLanguages.contains(where: { $0.id == language.id}) { + botPreviewLanguages.append(language) + assumeEmpty = true + } + botPreviewLanguages.sort(by: { $0.name < $1.name }) + self.removedBotPreviewLanguages.remove(language.id) + + if self.currentBotPreviewLanguages != botPreviewLanguages { + self.currentBotPreviewLanguages = botPreviewLanguages + if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: .immediate) + } + } + + self.setBotPreviewLanguage(id: language.id, assumeEmpty: assumeEmpty) + } + + private func deleteBotPreviewLanguage(id: String) { + var botPreviewLanguages = self.currentBotPreviewLanguages + + if let index = botPreviewLanguages.firstIndex(where: { $0.id == id}) { + botPreviewLanguages.remove(at: index) + } + self.removedBotPreviewLanguages.insert(id) + + guard case let .botPreview(peerId) = self.scope else { + return + } + + var mappedMedia: [Media] = [] + if let items = self.items { + mappedMedia = items.items.compactMap { item -> Media? in + guard let item = item as? VisualMediaItem else { + return nil + } + return item.story.media._asMedia() + } + } + let _ = self.context.engine.messages.deleteBotPreviewsLanguage(peerId: peerId, language: id, media: mappedMedia).startStandalone() + + if self.currentBotPreviewLanguages != botPreviewLanguages { + self.currentBotPreviewLanguages = botPreviewLanguages + if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: .immediate) + } + } + + self.setBotPreviewLanguage(id: nil, assumeEmpty: false) + } + + private func setBotPreviewLanguage(id: String?, assumeEmpty: Bool) { + guard case let .botPreview(peerId) = self.scope else { + return + } + if let listSource = self.listSource as? BotPreviewStoryListContext, listSource.language == id { + return + } + + if let id { + if let cachedListSource = self.cachedListSources[id] { + self.listSource = cachedListSource + } else { + let listSource = BotPreviewStoryListContext(account: self.context.account, engine: self.context.engine, peerId: peerId, language: id, assumeEmpty: assumeEmpty) + self.listSource = listSource + self.cachedListSources[id] = listSource + } + } else { + self.listSource = self.defaultListSource + } + + self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: true) + } + + public func beginReordering() { + if let parentController = self.parentController as? PeerInfoScreen { + parentController.togglePaneIsReordering(isReordering: true) + } else { + self.updateIsReordering(isReordering: true, animated: true) + } + } + + public func endReordering() { + if let parentController = self.parentController as? PeerInfoScreen { + parentController.togglePaneIsReordering(isReordering: false) + } else { + self.updateIsReordering(isReordering: false, animated: true) + } + } + + public func updateIsReordering(isReordering: Bool, animated: Bool) { + if self.isReordering != isReordering { + self.isReordering = isReordering + + self.contextGestureContainerNode.isGestureEnabled = self.isProfileEmbedded && self.itemInteraction.selectedIds == nil && !self.isReordering + + self.itemGrid.setReordering(isReordering: isReordering) + + if !isReordering, let reorderedIds = self.reorderedIds { + self.reorderedIds = nil + if case .botPreview = self.scope, let listSource = self.listSource as? BotPreviewStoryListContext { + if let items = self.items { + var reorderedMedia: [Media] = [] + + for id in reorderedIds { + if let item = items.items.first(where: { ($0 as? VisualMediaItem)?.storyId == id }) as? VisualMediaItem { + reorderedMedia.append(item.story.media._asMedia()) + } + } + + listSource.reorderItems(media: reorderedMedia) + } + } else if case let .peer(id, _, _) = self.scope, id == self.context.account.peerId, let items = self.items { + var updatedPinnedIds: [Int32] = [] + for id in reorderedIds { + inner: for item in items.items { + if let item = item as? VisualMediaItem { + if item.storyId == id { + if item.isPinned { + updatedPinnedIds.append(id.id) + break inner + } + } + } + } + } + let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: id, ids: updatedPinnedIds).startStandalone() + } + } + + self.update(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate) + } + } } private class MediaListSelectionRecognizer: UIPanGestureRecognizer { @@ -3825,7 +4652,6 @@ private final class BottomActionsPanelComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: BottomActionsPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme @@ -3905,7 +4731,9 @@ private final class BottomActionsPanelComponent: Component { let itemCenterX: CGFloat = CGFloat(i) * (floor((availableSize.width - sideInset * 2.0) / CGFloat(itemsAndSizes.count - 1))) let itemX: CGFloat - if i == 0 { + if itemsAndSizes.count == 1 { + itemX = floor((availableSize.width - itemSize.width) * 0.5) + } else if i == 0 { itemX = sideInset } else if i == itemsAndSizes.count - 1 { itemX = availableSize.width - sideInset - itemSize.width diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index 8d31667d61b..f43cb26bcaf 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -331,6 +331,10 @@ private final class GenericItemLayer: CALayer, ItemLayer { self.contentsGravity = .resize } + + override init(layer: Any) { + super.init(layer: layer) + } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") @@ -995,6 +999,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme return .never() } } + + func reorderIfPossible(item: SparseItemGrid.Item, toIndex: Int) { + } func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) { guard let item = item as? VisualMediaItem else { diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index c2986af79e3..26e890145f4 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -711,7 +711,9 @@ final class PeerSelectionControllerNode: ASDisplayNode { messageEffect: nil, attachment: false, canSendWhenOnline: false, - forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] + forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], + canMakePaidContent: false, + currentPrice: nil )), hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, @@ -733,6 +735,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { schedule: { [weak textInputPanelNode] messageEffect in textInputPanelNode?.sendMessage(.schedule, messageEffect) }, + editPrice: { _ in }, openPremiumPaywall: { [weak controller] c in guard let controller else { return diff --git a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift index 094d6f7c8ff..91fbf412057 100644 --- a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift @@ -22,42 +22,42 @@ public final class GiftAvatarComponent: Component { let theme: PresentationTheme let peers: [EnginePeer] let photo: TelegramMediaWebFile? - let starsPeer: StarsContext.State.Transaction.Peer? let isVisible: Bool let hasIdleAnimations: Bool let hasScaleAnimation: Bool let avatarSize: CGFloat let color: UIColor? let offset: CGFloat? + var hasLargeParticles: Bool public init( context: AccountContext, theme: PresentationTheme, peers: [EnginePeer], photo: TelegramMediaWebFile? = nil, - starsPeer: StarsContext.State.Transaction.Peer? = nil, isVisible: Bool, hasIdleAnimations: Bool, hasScaleAnimation: Bool = true, avatarSize: CGFloat = 100.0, color: UIColor? = nil, - offset: CGFloat? = nil + offset: CGFloat? = nil, + hasLargeParticles: Bool = false ) { self.context = context self.theme = theme self.peers = peers self.photo = photo - self.starsPeer = starsPeer self.isVisible = isVisible self.hasIdleAnimations = hasIdleAnimations self.hasScaleAnimation = hasScaleAnimation self.avatarSize = avatarSize self.color = color self.offset = offset + self.hasLargeParticles = hasLargeParticles } public static func ==(lhs: GiftAvatarComponent, rhs: GiftAvatarComponent) -> Bool { - return lhs.peers == rhs.peers && lhs.photo == rhs.photo && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.hasScaleAnimation == rhs.hasScaleAnimation && lhs.avatarSize == rhs.avatarSize && lhs.offset == rhs.offset + return lhs.peers == rhs.peers && lhs.photo == rhs.photo && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.hasScaleAnimation == rhs.hasScaleAnimation && lhs.avatarSize == rhs.avatarSize && lhs.offset == rhs.offset && lhs.hasLargeParticles == rhs.hasLargeParticles } public final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { @@ -142,7 +142,7 @@ public final class GiftAvatarComponent: Component { private var didSetup = false private func setup() { - guard let scene = loadCompressedScene(name: "gift", version: sceneVersion), !self.didSetup else { + guard let scene = loadCompressedScene(name: "gift2", version: sceneVersion), !self.didSetup else { return } @@ -152,6 +152,21 @@ public final class GiftAvatarComponent: Component { self.sceneView.delegate = self if let color = self.component?.color { +// let names: [String] = [ +// "particles_left", +// "particles_right", +// "particles_left_bottom", +// "particles_right_bottom", +// "particles_center" +// ] +// +// for name in names { +// if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first { +// particleSystem.particleColor = color +// particleSystem.particleColorVariation = SCNVector4Make(0, 0, 0, 0) +// } +// } + let names: [String] = [ "particles_left", "particles_right", @@ -160,10 +175,59 @@ public final class GiftAvatarComponent: Component { "particles_center" ] + let starNames: [String] = [ + "coins_left", + "coins_right" + ] + + let particleColor = color + for name in starNames { + if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first { + particleSystem.particleIntensity = 1.0 + particleSystem.particleIntensityVariation = 0.05 + particleSystem.particleColor = particleColor + particleSystem.particleColorVariation = SCNVector4Make(0.07, 0.0, 0.1, 0.0) + node.isHidden = false + + if let propertyControllers = particleSystem.propertyControllers, let sizeController = propertyControllers[.size], let colorController = propertyControllers[.color] { + let animation = CAKeyframeAnimation() + if let existing = colorController.animation as? CAKeyframeAnimation { + animation.keyTimes = existing.keyTimes + animation.values = existing.values?.compactMap { ($0 as? UIColor)?.alpha } ?? [] + } else { + animation.values = [ 0.0, 1.0, 1.0, 0.0 ] + } + let opacityController = SCNParticlePropertyController(animation: animation) + particleSystem.propertyControllers = [ + .size: sizeController, + .opacity: opacityController + ] + } + } + } + for name in names { if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first { - particleSystem.particleColor = color - particleSystem.particleColorVariation = SCNVector4Make(0, 0, 0, 0) + particleSystem.particleIntensity = min(1.0, 2.0 * particleSystem.particleIntensity) + particleSystem.particleIntensityVariation = 0.05 + particleSystem.particleColor = particleColor + particleSystem.particleColorVariation = SCNVector4Make(0.1, 0.0, 0.12, 0.0) + + + if let propertyControllers = particleSystem.propertyControllers, let sizeController = propertyControllers[.size], let colorController = propertyControllers[.color] { + let animation = CAKeyframeAnimation() + if let existing = colorController.animation as? CAKeyframeAnimation { + animation.keyTimes = existing.keyTimes + animation.values = existing.values?.compactMap { ($0 as? UIColor)?.alpha } ?? [] + } else { + animation.values = [ 0.0, 1.0, 1.0, 0.0 ] + } + let opacityController = SCNParticlePropertyController(animation: animation) + particleSystem.propertyControllers = [ + .size: sizeController, + .opacity: opacityController + ] + } } } @@ -187,9 +251,7 @@ public final class GiftAvatarComponent: Component { } } - private func onReady() { - self.setupScaleAnimation() - + private func onReady() { self.playAppearanceAnimation(explode: true) self.previousInteractionTimestamp = CACurrentMediaTime() @@ -203,23 +265,7 @@ public final class GiftAvatarComponent: Component { }, queue: Queue.mainQueue()) self.timer?.start() } - - private func setupScaleAnimation() { - guard self.component?.hasScaleAnimation == true else { - return - } - - let animation = CABasicAnimation(keyPath: "transform.scale") - animation.duration = 2.0 - animation.fromValue = 1.0 - animation.toValue = 1.15 - animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - animation.autoreverses = true - animation.repeatCount = .infinity - - self.avatarNode.view.layer.add(animation, forKey: "scale") - } - + private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) { guard let scene = self.sceneView.scene else { return @@ -319,6 +365,10 @@ public final class GiftAvatarComponent: Component { self.hasIdleAnimations = component.hasIdleAnimations + if let _ = component.color { + self.sceneView.backgroundColor = component.theme.list.blocksBackgroundColor + } + if let photo = component.photo { let imageNode: TransformImageNode if let current = self.imageNode { @@ -339,86 +389,6 @@ public final class GiftAvatarComponent: Component { imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() self.avatarNode.isHidden = true - } else if let starsPeer = component.starsPeer { - let iconBackgroundView: UIImageView - let iconView: UIImageView - if let currentBackground = self.iconBackgroundView, let current = self.iconView { - iconBackgroundView = currentBackground - iconView = current - } else { - iconBackgroundView = UIImageView() - iconView = UIImageView() - - self.addSubview(iconBackgroundView) - self.addSubview(iconView) - - self.iconBackgroundView = iconBackgroundView - self.iconView = iconView - - let size = CGSize(width: component.avatarSize, height: component.avatarSize) - var iconInset: CGFloat = 9.0 - var iconOffset: CGFloat = 0.0 - - switch starsPeer { - case .appStore: - iconBackgroundView.image = generateGradientFilledCircleImage( - diameter: size.width, - colors: [ - UIColor(rgb: 0x2a9ef1).cgColor, - UIColor(rgb: 0x72d5fd).cgColor - ], - direction: .mirroredDiagonal - ) - iconView.image = UIImage(bundleImageName: "Premium/Stars/Apple") - case .playMarket: - iconBackgroundView.image = generateGradientFilledCircleImage( - diameter: size.width, - colors: [ - UIColor(rgb: 0x54cb68).cgColor, - UIColor(rgb: 0xa0de7e).cgColor - ], - direction: .mirroredDiagonal - ) - iconView.image = UIImage(bundleImageName: "Premium/Stars/Google") - case .fragment: - iconBackgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24)) - iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment") - iconOffset = 5.0 - case .ads: - iconBackgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24)) - iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment") - iconOffset = 5.0 - case .premiumBot: - iconInset = 15.0 - iconBackgroundView.image = generateGradientFilledCircleImage( - diameter: size.width, - colors: [ - UIColor(rgb: 0x6b93ff).cgColor, - UIColor(rgb: 0x6b93ff).cgColor, - UIColor(rgb: 0x8d77ff).cgColor, - UIColor(rgb: 0xb56eec).cgColor, - UIColor(rgb: 0xb56eec).cgColor - ], - direction: .mirroredDiagonal - ) - iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) - case .peer, .unsupported: - iconInset = 15.0 - iconBackgroundView.image = generateGradientFilledCircleImage( - diameter: size.width, - colors: [ - UIColor(rgb: 0xb1b1b1).cgColor, - UIColor(rgb: 0xcdcdcd).cgColor - ], - direction: .mirroredDiagonal - ) - iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) - } - - let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - size.width) / 2.0), y: 113.0 - size.height / 2.0), size: size) - iconBackgroundView.frame = imageFrame - iconView.frame = imageFrame.insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset) - } } else if component.peers.count > 1 { let avatarSize = CGSize(width: 60.0, height: 60.0) diff --git a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift index 102e7b1fd69..520c5d79da6 100644 --- a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift +++ b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift @@ -17,6 +17,7 @@ public class PremiumGiftAttachmentScreen: PremiumGiftScreen, AttachmentContainab public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } + public var isMinimized: Bool = false public var mediaPickerContext: AttachmentMediaPickerContext? { return PremiumGiftContext(controller: self) @@ -25,30 +26,7 @@ public class PremiumGiftAttachmentScreen: PremiumGiftScreen, AttachmentContainab private final class PremiumGiftContext: AttachmentMediaPickerContext { private weak var controller: PremiumGiftScreen? - - var selectionCount: Signal { - return .single(0) - } - - var caption: Signal { - return .single(nil) - } - - var hasCaption: Bool { - return false - } - - var captionIsAboveMedia: Signal { - return .single(false) - } - - func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { - } - - public var loadingProgress: Signal { - return .single(nil) - } - + public var mainButtonState: Signal { return self.controller?.mainButtonStatePromise.get() ?? .single(nil) } @@ -56,15 +34,6 @@ private final class PremiumGiftContext: AttachmentMediaPickerContext { init(controller: PremiumGiftScreen) { self.controller = controller } - - func setCaption(_ caption: NSAttributedString) { - } - - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { - } - - func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { - } func mainButtonAction() { self.controller?.mainButtonPressed() diff --git a/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift b/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift index f080c7b8d88..0bf4adb63a3 100644 --- a/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift +++ b/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift @@ -913,6 +913,7 @@ private extension MediaEditorValues { audioTrackOffset: nil, audioTrackVolume: nil, audioTrackSamples: nil, + coverImageTimestamp: nil, qualityPreset: qualityPreset ) } @@ -1081,6 +1082,7 @@ private extension MediaEditorValues { audioTrackOffset: nil, audioTrackVolume: nil, audioTrackSamples: nil, + coverImageTimestamp: nil, qualityPreset: qualityPreset ) } diff --git a/submodules/TelegramUI/Components/SaveProgressScreen/BUILD b/submodules/TelegramUI/Components/SaveProgressScreen/BUILD new file mode 100644 index 00000000000..e9de562525e --- /dev/null +++ b/submodules/TelegramUI/Components/SaveProgressScreen/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SaveProgressScreen", + module_name = "SaveProgressScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/LottieAnimationComponent", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift b/submodules/TelegramUI/Components/SaveProgressScreen/Sources/SaveProgressScreen.swift similarity index 100% rename from submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift rename to submodules/TelegramUI/Components/SaveProgressScreen/Sources/SaveProgressScreen.swift diff --git a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift index 9cbdf82a8a6..4d8f5ecebdd 100644 --- a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift +++ b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift @@ -1012,7 +1012,8 @@ public class SendInviteLinkScreen: ViewControllerComponentContainer { nameColor: user.nameColor, backgroundEmojiId: user.backgroundEmojiId, profileColor: user.profileColor, - profileBackgroundEmojiId: user.profileBackgroundEmojiId + profileBackgroundEmojiId: user.profileBackgroundEmojiId, + subscriberCount: user.subscriberCount )), canInviteWithPremium: canInviteWithPremium, premiumRequiredToContact: premiumRequiredToContact diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 9409f8a6a11..61127abfb45 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -1317,6 +1317,7 @@ public final class QuickReplySetupScreen: ViewControllerComponentContainer, Atta public var isContainerExpanded: () -> Bool = { return false } + public var isMinimized: Bool = false public var mediaPickerContext: AttachmentMediaPickerContext? public init(context: AccountContext, initialData: InitialData, mode: Mode) { diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/BUILD b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/BUILD new file mode 100644 index 00000000000..802cc7a542b --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/BUILD @@ -0,0 +1,31 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "LanguageSelectionScreen", + module_name = "LanguageSelectionScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/MergeLists", + "//submodules/ItemListUI", + "//submodules/PresentationDataUtils", + "//submodules/AccountContext", + "//submodules/SearchBarNode", + "//submodules/SearchUI", + "//submodules/TelegramUIPreferences", + "//submodules/TranslateUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift new file mode 100644 index 00000000000..9d698608daa --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift @@ -0,0 +1,178 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import AccountContext +import SearchUI + +public class LanguageSelectionScreen: ViewController { + private let context: AccountContext + private let excludeIds: [String] + private let selectLocalization: (LocalizationInfo) -> Void + + private var controllerNode: LanguageSelectionScreenNode { + return self.displayNode as! LanguageSelectionScreenNode + } + + private var _ready = Promise() + override public var ready: Promise { + return self._ready + } + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private var searchContentNode: NavigationBarSearchContentNode? + + private var previousContentOffset: ListViewVisibleContentOffset? + + public init(context: AccountContext, excludeIds: [String] = [], selectLocalization: @escaping (LocalizationInfo) -> Void) { + self.context = context + self.excludeIds = excludeIds + self.selectLocalization = selectLocalization + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self.navigationPresentation = .modal + + self.title = self.presentationData.strings.BotPreviews_SelectLanguage_Title + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.scrollToTop = { [weak self] in + if let strongSelf = self { + if let searchContentNode = strongSelf.searchContentNode { + searchContentNode.updateExpansionProgress(1.0, animated: true) + } + strongSelf.controllerNode.scrollToTop() + } + } + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings() + } + } + }) + + self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, inline: true, activate: { [weak self] in + self?.activateSearch() + }) + self.navigationBar?.setContentNode(self.searchContentNode, animated: false) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) + self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search) + self.title = self.presentationData.strings.Settings_AppLanguage + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + self.controllerNode.updatePresentationData(self.presentationData) + } + + override public func loadDisplayNode() { + self.displayNode = LanguageSelectionScreenNode(context: self.context, presentationData: self.presentationData, navigationBar: self.navigationBar!, excludeIds: self.excludeIds, requestActivateSearch: { [weak self] in + self?.activateSearch() + }, requestDeactivateSearch: { [weak self] in + self?.deactivateSearch() + }, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }, push: { [weak self] c in + self?.push(c) + }, selectLocalization: { [weak self] info in + guard let self else { + return + } + self.selectLocalization(info) + self.dismiss() + }) + + self.controllerNode.listNode.visibleContentOffsetChanged = { [weak self] offset in + if let strongSelf = self { + if let searchContentNode = strongSelf.searchContentNode { + searchContentNode.updateListVisibleContentOffset(offset) + } + + var previousContentOffsetValue: CGFloat? + if let previousContentOffset = strongSelf.previousContentOffset, case let .known(value) = previousContentOffset { + previousContentOffsetValue = value + } + switch offset { + case let .known(value): + let transition: ContainedViewLayoutTransition + if let previousContentOffsetValue = previousContentOffsetValue, value <= 0.0, previousContentOffsetValue > 30.0 { + transition = .animated(duration: 0.2, curve: .easeInOut) + } else { + transition = .immediate + } + strongSelf.navigationBar?.updateBackgroundAlpha(min(30.0, max(0.0, value - 54.0)) / 30.0, transition: transition) + case .unknown, .none: + strongSelf.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate) + } + + strongSelf.previousContentOffset = offset + } + } + + self.controllerNode.listNode.didEndScrolling = { [weak self] _ in + if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode { + let _ = fixNavigationSearchableListNodeScrolling(strongSelf.controllerNode.listNode, searchNode: searchContentNode) + } + } + + self._ready.set(self.controllerNode._ready.get()) + + self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) + + self.displayNodeDidLoad() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, transition: transition) + } + + private func activateSearch() { + if self.displayNavigationBar { + if let scrollToTop = self.scrollToTop { + scrollToTop() + } + if let searchContentNode = self.searchContentNode { + self.controllerNode.activateSearch(placeholderNode: searchContentNode.placeholderNode) + } + self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring)) + } + } + + private func deactivateSearch() { + if !self.displayNavigationBar { + self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) + if let searchContentNode = self.searchContentNode { + self.controllerNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode) + } + } + } +} diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift new file mode 100644 index 00000000000..cd14148e2ec --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift @@ -0,0 +1,568 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import TelegramPresentationData +import MergeLists +import ItemListUI +import PresentationDataUtils +import AccountContext +import SearchBarNode +import SearchUI +import TelegramUIPreferences +import TranslateUI + +private enum LanguageListSection: ItemListSectionId { + case official + case unofficial +} + +private enum LanguageListEntryId: Hashable { + case localizationTitle + case localization(String) +} + +private enum LanguageListEntryType { + case official + case unofficial +} + +private enum LanguageListEntry: Comparable, Identifiable { + case localizationTitle(text: String, section: ItemListSectionId) + case localization(index: Int, info: LocalizationInfo?, type: LanguageListEntryType, isEnabled: Bool) + + var stableId: LanguageListEntryId { + switch self { + case .localizationTitle: + return .localizationTitle + case let .localization(index, info, _, _): + return .localization(info?.languageCode ?? "\(index)") + } + } + + private func index() -> Int { + switch self { + case .localizationTitle: + return 1000 + case let .localization(index, _, _, _): + return 1001 + index + } + } + + static func <(lhs: LanguageListEntry, rhs: LanguageListEntry) -> Bool { + return lhs.index() < rhs.index() + } + + func item(presentationData: PresentationData, searchMode: Bool, openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void) -> ListViewItem { + switch self { + case let .localizationTitle(text, section): + return ItemListSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), text: text, sectionId: section) + case let .localization(_, info, type, isEnabled): + return LocalizationListItem(presentationData: ItemListPresentationData(presentationData), id: info?.languageCode ?? "", title: info?.title ?? " ", subtitle: info?.localizedTitle ?? " ", checked: false, activity: false, loading: info == nil, editing: LocalizationListItemEditing(editable: false, editing: false, revealed: false, reorderable: false), enabled: isEnabled, sectionId: type == .official ? LanguageListSection.official.rawValue : LanguageListSection.unofficial.rawValue, alwaysPlain: searchMode, action: { + if let info { + selectLocalization(info) + } + }, setItemWithRevealedOptions: nil, removeItem: nil) + } + } +} + +private struct LocalizationListSearchContainerTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let isSearching: Bool +} + +private func preparedLanguageListSearchContainerTransition(presentationData: PresentationData, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], selectLocalization: @escaping (LocalizationInfo) -> Void, isSearching: Bool, forceUpdate: Bool) -> LocalizationListSearchContainerTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, selectLocalization: selectLocalization), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, selectLocalization: selectLocalization), directionHint: nil) } + + return LocalizationListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) +} + +private final class LocalizationListSearchContainerNode: SearchDisplayControllerContentNode { + private let dimNode: ASDisplayNode + private let listNode: ListView + + private var enqueuedTransitions: [LocalizationListSearchContainerTransition] = [] + private var hasValidLayout = false + + private let searchQuery = Promise() + private let searchDisposable = MetaDisposable() + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private let presentationDataPromise: Promise + + public override var hasDim: Bool { + return true + } + + init(context: AccountContext, listState: LocalizationListState, excludedIds: [String], selectLocalization: @escaping (LocalizationInfo) -> Void) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationData = presentationData + + self.presentationDataPromise = Promise(self.presentationData) + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) + + self.listNode = ListView() + self.listNode.accessibilityPageScrolledString = { row, count in + return presentationData.strings.VoiceOver_ScrollStatus(row, count).string + } + + super.init() + + self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor + self.listNode.isHidden = true + + self.addSubnode(self.dimNode) + self.addSubnode(self.listNode) + + let foundItems = self.searchQuery.get() + |> mapToSignal { query -> Signal<[LocalizationInfo]?, NoError> in + if let query = query, !query.isEmpty { + let normalizedQuery = query.lowercased() + var result: [LocalizationInfo] = [] + var uniqueIds = Set() + for info in listState.availableSavedLocalizations + listState.availableOfficialLocalizations { + if info.title.lowercased().hasPrefix(normalizedQuery) || info.localizedTitle.lowercased().hasPrefix(normalizedQuery) { + if uniqueIds.contains(info.languageCode) { + continue + } + uniqueIds.insert(info.languageCode) + result.append(info) + } + } + return .single(result) + } else { + return .single(nil) + } + } + + let previousEntriesHolder = Atomic<([LanguageListEntry], PresentationTheme, PresentationStrings)?>(value: nil) + self.searchDisposable.set(combineLatest(queue: .mainQueue(), foundItems, self.presentationDataPromise.get()).start(next: { [weak self] items, presentationData in + guard let strongSelf = self else { + return + } + var entries: [LanguageListEntry] = [] + if let items = items { + for item in items { + entries.append(.localization(index: entries.count, info: item, type: .official, isEnabled: !excludedIds.contains(item.languageCode))) + } + } + let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) + let transition = preparedLanguageListSearchContainerTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, selectLocalization: selectLocalization, isSearching: items != nil, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings) + strongSelf.enqueueTransition(transition) + })) + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) + strongSelf.presentationDataPromise.set(.single(presentationData)) + } + } + }) + + self.listNode.beganInteractiveDragging = { [weak self] _ in + self?.dismissInput?() + } + } + + deinit { + self.searchDisposable.dispose() + self.presentationDataDisposable?.dispose() + } + + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.listNode.backgroundColor = theme.chatList.backgroundColor + } + + override func searchTextUpdated(text: String) { + if text.isEmpty { + self.searchQuery.set(.single(nil)) + } else { + self.searchQuery.set(.single(text)) + } + } + + private func enqueueTransition(_ transition: LocalizationListSearchContainerTransition) { + self.enqueuedTransitions.append(transition) + + if self.hasValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransitions() + } + } + } + + private func dequeueTransitions() { + if let transition = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + options.insert(.PreferSynchronousDrawing) + + let isSearching = transition.isSearching + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + self?.listNode.isHidden = !isSearching + self?.dimNode.isHidden = isSearching + }) + } + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + let topInset = navigationBarHeight + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + + self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !self.hasValidLayout { + self.hasValidLayout = true + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransitions() + } + } + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancel?() + } + } +} + +private struct LanguageListNodeTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let firstTime: Bool + let isLoading: Bool + let animated: Bool + let crossfade: Bool +} + +private func preparedLanguageListNodeTransition( + presentationData: PresentationData, + from fromEntries: [LanguageListEntry], + to toEntries: [LanguageListEntry], + openSearch: @escaping () -> Void, + selectLocalization: @escaping (LocalizationInfo) -> Void, + firstTime: Bool, + isLoading: Bool, + forceUpdate: Bool, + animated: Bool, + crossfade: Bool +) -> LanguageListNodeTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization), directionHint: nil) } + + return LanguageListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, isLoading: isLoading, animated: animated, crossfade: crossfade) +} + +final class LanguageSelectionScreenNode: ViewControllerTracingNode { + private let context: AccountContext + private var presentationData: PresentationData + private weak var navigationBar: NavigationBar? + private let excludeIds: [String] + private let requestActivateSearch: () -> Void + private let requestDeactivateSearch: () -> Void + private let present: (ViewController, Any?) -> Void + private let push: (ViewController) -> Void + private let selectLocalization: (LocalizationInfo) -> Void + + private var didSetReady = false + let _ready = ValuePromise() + + private var containerLayout: (ContainerViewLayout, CGFloat)? + let listNode: ListView + private let leftOverlayNode: ASDisplayNode + private let rightOverlayNode: ASDisplayNode + private var queuedTransitions: [LanguageListNodeTransition] = [] + private var searchDisplayController: SearchDisplayController? + + private let presentationDataValue = Promise() + private var updatedDisposable: Disposable? + private var listDisposable: Disposable? + + private var currentListState: LocalizationListState? + + init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, excludeIds: [String], requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void) { + self.context = context + self.presentationData = presentationData + self.presentationDataValue.set(.single(presentationData)) + self.navigationBar = navigationBar + self.excludeIds = excludeIds + self.requestActivateSearch = requestActivateSearch + self.requestDeactivateSearch = requestDeactivateSearch + self.present = present + self.push = push + self.selectLocalization = selectLocalization + + self.listNode = ListView() + self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.list.blocksBackgroundColor, direction: true) + self.listNode.accessibilityPageScrolledString = { row, count in + return presentationData.strings.VoiceOver_ScrollStatus(row, count).string + } + self.leftOverlayNode = ASDisplayNode() + self.leftOverlayNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor + self.rightOverlayNode = ASDisplayNode() + self.rightOverlayNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor + + super.init() + + self.backgroundColor = presentationData.theme.list.blocksBackgroundColor + self.addSubnode(self.listNode) + + let openSearch: () -> Void = { + requestActivateSearch() + } + + let previousState = Atomic(value: nil) + let previousEntriesHolder = Atomic<([LanguageListEntry], PresentationTheme, PresentationStrings)?>(value: nil) + self.listDisposable = combineLatest( + queue: .mainQueue(), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.LocalizationList()), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), + context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.localizationSettings, ApplicationSpecificSharedDataKeys.translationSettings]), + self.presentationDataValue.get() + ).start(next: { [weak self] localizationListState, peer, sharedData, presentationData in + guard let strongSelf = self else { + return + } + + var entries: [LanguageListEntry] = [] + var existingIds = Set() + + if !localizationListState.availableOfficialLocalizations.isEmpty { + strongSelf.currentListState = localizationListState + + let availableSavedLocalizations = localizationListState.availableSavedLocalizations.filter({ info in !localizationListState.availableOfficialLocalizations.contains(where: { $0.languageCode == info.languageCode }) }) + if !availableSavedLocalizations.isEmpty { + for info in availableSavedLocalizations { + if existingIds.contains(info.languageCode) { + continue + } + existingIds.insert(info.languageCode) + entries.append(.localization(index: entries.count, info: info, type: .unofficial, isEnabled: !strongSelf.excludeIds.contains(info.languageCode))) + } + } + for info in localizationListState.availableOfficialLocalizations { + if existingIds.contains(info.languageCode) { + continue + } + existingIds.insert(info.languageCode) + entries.append(.localization(index: entries.count, info: info, type: .official, isEnabled: !strongSelf.excludeIds.contains(info.languageCode))) + } + } else { + for _ in 0 ..< 15 { + entries.append(.localization(index: entries.count, info: nil, type: .official, isEnabled: true)) + } + } + + let previousState = previousState.swap(localizationListState) + + let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) + let transition = preparedLanguageListNodeTransition( + presentationData: presentationData, + from: previousEntriesAndPresentationData?.0 ?? [], + to: entries, + openSearch: openSearch, + selectLocalization: { [weak self] info in + self?.selectLocalization(info) + }, + firstTime: previousEntriesAndPresentationData == nil, + isLoading: entries.isEmpty, + forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, + animated: (previousEntriesAndPresentationData?.0.count ?? 0) != entries.count, + crossfade: (previousState == nil || previousState!.availableOfficialLocalizations.isEmpty) != localizationListState.availableOfficialLocalizations.isEmpty + ) + strongSelf.enqueueTransition(transition) + }) + self.updatedDisposable = context.engine.localization.synchronizedLocalizationListState().start() + + self.listNode.itemNodeHitTest = { [weak self] point in + if let strongSelf = self { + return point.x > strongSelf.leftOverlayNode.frame.maxX && point.x < strongSelf.rightOverlayNode.frame.minX + } else { + return true + } + } + } + + deinit { + self.listDisposable?.dispose() + self.updatedDisposable?.dispose() + } + + func updatePresentationData(_ presentationData: PresentationData) { + let stringsUpdated = self.presentationData.strings !== presentationData.strings + self.presentationData = presentationData + + if stringsUpdated { + if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) { + self.view.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + + self.presentationDataValue.set(.single(presentationData)) + self.backgroundColor = presentationData.theme.list.blocksBackgroundColor + self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.list.blocksBackgroundColor, direction: true) + self.searchDisplayController?.updatePresentationData(presentationData) + self.leftOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor + self.rightOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let hadValidLayout = self.containerLayout != nil + self.containerLayout = (layout, navigationBarHeight) + + var listInsets = layout.insets(options: [.input]) + listInsets.top += navigationBarHeight + if layout.size.width >= 375.0 { + let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) + listInsets.left += inset + listInsets.right += inset + } else { + listInsets.left += layout.safeInsets.left + listInsets.right += layout.safeInsets.right + } + + self.leftOverlayNode.frame = CGRect(x: 0.0, y: 0.0, width: listInsets.left, height: layout.size.height) + self.rightOverlayNode.frame = CGRect(x: layout.size.width - listInsets.right, y: 0.0, width: listInsets.right, height: layout.size.height) + + if self.leftOverlayNode.supernode == nil { + self.insertSubnode(self.leftOverlayNode, aboveSubnode: self.listNode) + } + if self.rightOverlayNode.supernode == nil { + self.insertSubnode(self.rightOverlayNode, aboveSubnode: self.listNode) + } + + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, duration: duration, curve: curve) + + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !hadValidLayout { + self.dequeueTransitions() + } + } + + private func enqueueTransition(_ transition: LanguageListNodeTransition) { + self.queuedTransitions.append(transition) + + if self.containerLayout != nil { + self.dequeueTransitions() + } + } + + private func dequeueTransitions() { + guard let _ = self.containerLayout else { + return + } + while !self.queuedTransitions.isEmpty { + let transition = self.queuedTransitions.removeFirst() + + var options = ListViewDeleteAndInsertOptions() + if transition.firstTime { + options.insert(.Synchronous) + options.insert(.LowLatency) + } else if transition.crossfade { + options.insert(.AnimateCrossfade) + } else if transition.animated { + options.insert(.AnimateInsertion) + } + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in + if let strongSelf = self { + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf._ready.set(true) + } + } + }) + } + } + + func activateSearch(placeholderNode: SearchBarPlaceholderNode) { + guard let (containerLayout, navigationBarHeight) = self.containerLayout, self.searchDisplayController == nil else { + return + } + + self.searchDisplayController = SearchDisplayController( + presentationData: self.presentationData, + contentNode: LocalizationListSearchContainerNode( + context: self.context, + listState: self.currentListState ?? LocalizationListState.defaultSettings, + excludedIds: self.excludeIds, + selectLocalization: { [weak self] info in + self?.selectLocalization(info) + }), + inline: true, + cancel: { [weak self] in + self?.requestDeactivateSearch() + } + ) + + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in + if let strongSelf = self, let strongPlaceholderNode = placeholderNode { + if isSearchBar { + strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode) + } else if let navigationBar = strongSelf.navigationBar { + strongSelf.insertSubnode(subnode, belowSubnode: navigationBar) + } + } + }, placeholder: placeholderNode) + } + + func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) { + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.deactivate(placeholder: placeholderNode) + self.searchDisplayController = nil + } + } + + func scrollToTop() { + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } +} diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift index c96c73c3a56..2d28458d851 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift @@ -225,12 +225,12 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { var peers = SimpleDictionary() var messages = SimpleDictionary() - peers[authorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: messageItem.author, lastName: "", username: nil, phone: nil, photo: messageItem.photo, botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil) + peers[authorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: messageItem.author, lastName: "", username: nil, phone: nil, photo: messageItem.photo, botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) if let (replyAuthor, text, replyColor) = messageItem.reply { - peers[replyAuthorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: replyColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil) + peers[replyAuthorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: replyColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[replyAuthorPeerId], text: text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) } diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift index 7b2d0511a5c..407a1b3eae0 100644 --- a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift @@ -159,7 +159,7 @@ final class PeerSelectionLoadingView: UIView { if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData { self.currentParams = (size, presentationData) - let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", 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)) + let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", 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 items = (0 ..< 1).map { _ -> ContactsPeerItem in return ContactsPeerItem( diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift index c1b99534e14..658f8e70b70 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift @@ -278,7 +278,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode { var peers = SimpleDictionary() let messages = SimpleDictionary() - peers[userPeerId] = TelegramUser(id: userPeerId, accessHash: nil, firstName: item.strings.Settings_QuickReactionSetup_DemoMessageAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + peers[userPeerId] = TelegramUser(id: userPeerId, accessHash: nil, firstName: item.strings.Settings_QuickReactionSetup_DemoMessageAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) let messageText = item.strings.Settings_QuickReactionSetup_DemoMessageText diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 7c4c1625691..0ce8b9d7982 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -954,12 +954,12 @@ 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)) - 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)) - 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)) + 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 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)) - 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)) + 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 timestamp = self.referenceTimestamp @@ -1047,8 +1047,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate let otherPeerId = self.context.account.peerId var peers = SimpleDictionary() var messages = SimpleDictionary() - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) - peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) + peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) var sampleMessages: [Message] = [] @@ -1093,7 +1093,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate state.displayPatternPanel = false return state }, animated: true) - }, clickThroughMessage: { + }, clickThroughMessage: { _, _ in }, backgroundNode: self.backgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false) return item } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift index 62e79340c70..5047fb1cf60 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift @@ -1513,8 +1513,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let replyAuthor = self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName - var messageAuthor: Peer = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_PreviewReplyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) - let otherAuthor = TelegramUser(id: otherPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + var messageAuthor: Peer = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_PreviewReplyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) + let otherAuthor = TelegramUser(id: otherPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) peers[otherPeerId] = otherAuthor var messageAttributes: [MessageAttribute] = [] diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift index a6fcaa6510e..bc1c23f0ae8 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift @@ -345,6 +345,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } + public var isMinimized: Bool = false public var mediaPickerContext: AttachmentMediaPickerContext? { return ThemeColorsGridContext(controller: self) @@ -354,29 +355,6 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina private final class ThemeColorsGridContext: AttachmentMediaPickerContext { private weak var controller: ThemeColorsGridController? - var selectionCount: Signal { - return .single(0) - } - - var caption: Signal { - return .single(nil) - } - - var hasCaption: Bool { - return false - } - - var captionIsAboveMedia: Signal { - return .single(false) - } - - func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { - } - - public var loadingProgress: Signal { - return .single(nil) - } - public var mainButtonState: Signal { return .single(self.controller?.mainButtonState) } @@ -385,15 +363,6 @@ private final class ThemeColorsGridContext: AttachmentMediaPickerContext { self.controller = controller } - func setCaption(_ caption: NSAttributedString) { - } - - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { - } - - func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { - } - func mainButtonAction() { self.controller?.mainButtonPressed() } diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CoverListItemComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CoverListItemComponent.swift new file mode 100644 index 00000000000..364a948ddcd --- /dev/null +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CoverListItemComponent.swift @@ -0,0 +1,144 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultilineTextComponent +import TelegramPresentationData +import SwitchComponent + +final class CoverListItemComponent: Component { + let theme: PresentationTheme + let title: String + let image: UIImage? + let hasNext: Bool + let action: () -> Void + + init( + theme: PresentationTheme, + title: String, + image: UIImage?, + hasNext: Bool, + action: @escaping () -> Void + ) { + self.theme = theme + self.title = title + self.image = image + self.hasNext = hasNext + self.action = action + } + + static func ==(lhs: CoverListItemComponent, rhs: CoverListItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.image !== rhs.image { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + final class View: UIView { + private let containerButton: HighlightTrackingButton + + private let title = ComponentView() + private let icon = ComponentView() + private let separatorLayer: SimpleLayer + + private var component: CoverListItemComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + self.containerButton = HighlightTrackingButton() + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + self.addSubview(self.containerButton) + + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action() + } + + func update(component: CoverListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + self.state = state + + let height: CGFloat = 44.0 + let verticalInset: CGFloat = 0.0 + let leftInset: CGFloat = 16.0 + let rightInset: CGFloat = 16.0 + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(Image(image: component.image, contentMode: .scaleAspectFill)), + environment: {}, + containerSize: CGSize(width: 30.0, height: 30.0) + ) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + } + if let iconView = self.icon.view { + if iconView.superview == nil { + iconView.clipsToBounds = true + iconView.layer.cornerRadius = 5.0 + self.containerButton.addSubview(iconView) + } + transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset - iconSize.width, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize)) + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + } + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext + + let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalInset), size: CGSize(width: availableSize.width, height: height - verticalInset * 2.0)) + transition.setFrame(view: self.containerButton, frame: containerFrame) + + return CGSize(width: availableSize.width, height: height) + } + } + + 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) + } +} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 130b1564602..e575681b3e7 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -38,9 +38,11 @@ final class ShareWithPeersScreenComponent: Component { let mentions: [String] let categoryItems: [CategoryItem] let optionItems: [OptionItem] + let coverItem: CoverItem? let completion: (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void let editCategory: (EngineStoryPrivacy, Bool, Bool) -> Void let editBlockedPeers: (EngineStoryPrivacy, Bool, Bool) -> Void + let editCover: () -> Void let peerCompletion: (EnginePeer.Id) -> Void init( @@ -54,9 +56,11 @@ final class ShareWithPeersScreenComponent: Component { mentions: [String], categoryItems: [CategoryItem], optionItems: [OptionItem], + coverItem: CoverItem?, completion: @escaping (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void, editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, + editCover: @escaping () -> Void, peerCompletion: @escaping (EnginePeer.Id) -> Void ) { self.context = context @@ -69,9 +73,11 @@ final class ShareWithPeersScreenComponent: Component { self.mentions = mentions self.categoryItems = categoryItems self.optionItems = optionItems + self.coverItem = coverItem self.completion = completion self.editCategory = editCategory self.editBlockedPeers = editBlockedPeers + self.editCover = editCover self.peerCompletion = peerCompletion } @@ -106,6 +112,9 @@ final class ShareWithPeersScreenComponent: Component { if lhs.optionItems != rhs.optionItems { return false } + if lhs.coverItem != rhs.coverItem { + return false + } return true } @@ -258,6 +267,33 @@ final class ShareWithPeersScreenComponent: Component { return false } } + + enum CoverId: Int, Hashable { + case choose = 0 + } + + final class CoverItem: Equatable { + let id: CoverId + let title: String + let image: UIImage? + + init( + id: CoverId, + title: String, + image: UIImage? + ) { + self.id = id + self.title = title + self.image = image + } + + static func ==(lhs: CoverItem, rhs: CoverItem) -> Bool { + if lhs === rhs { + return true + } + return false + } + } final class View: UIView, UIScrollViewDelegate { private let dimView: UIView @@ -956,7 +992,7 @@ final class ShareWithPeersScreenComponent: Component { let sectionTitle: String if section.id == 0, case .stories = component.stateContext.subject { - sectionTitle = environment.strings.Story_Privacy_PostStoryAsHeader + sectionTitle = component.coverItem == nil ? environment.strings.Story_Privacy_PostStoryAsHeader : "" } else if section.id == 2 { sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader } else if section.id == 1 { @@ -1607,6 +1643,93 @@ final class ShareWithPeersScreenComponent: Component { footerText = isSendAsGroup ? environment.strings.Story_Privacy_KeepOnGroupPageInfo(footerValue).string : environment.strings.Story_Privacy_KeepOnChannelPageInfo(footerValue).string } + let footerSize = sectionFooter.update( + transition: sectionFooterTransition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: footerText, font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor)), + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: itemLayout.containerSize.width - 16.0 * 2.0, height: itemLayout.contentHeight) + ) + let footerFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset + 16.0, y: sectionOffset + section.totalHeight + 7.0), size: footerSize) + if let footerView = sectionFooter.view { + if footerView.superview == nil { + self.itemContainerView.addSubview(footerView) + } + sectionFooterTransition.setFrame(view: footerView, frame: footerFrame) + } + sectionOffset += footerSize.height + } else if section.id == 4 && section.itemCount > 0 { + if let item = component.coverItem { + let itemFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: sectionOffset + section.insets.top + CGFloat(0) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) + if !visibleBounds.intersects(itemFrame) { + continue + } + + let itemId = AnyHashable(item.id) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.visibleItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.visibleItems[itemId] = visibleItem + } + + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent(CoverListItemComponent( + theme: environment.theme, + title: item.title, + image: item.image, + hasNext: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.editCover() + self.saveAndDismiss() + } + )), + environment: {}, + containerSize: itemFrame.size + ) + if let itemView = visibleItem.view { + if itemView.superview == nil { + if let minSectionHeader { + self.itemContainerView.insertSubview(itemView, belowSubview: minSectionHeader) + } else { + self.itemContainerView.addSubview(itemView) + } + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + } + + let sectionFooter: ComponentView + var sectionFooterTransition = transition + if let current = self.visibleSectionFooters[section.id] { + sectionFooter = current + } else { + if !transition.animation.isImmediate { + sectionFooterTransition = .immediate + } + sectionFooter = ComponentView() + self.visibleSectionFooters[section.id] = sectionFooter + } + + var footerText = environment.strings.Story_Privacy_ChooseCoverInfo + if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true { + footerText = isSendAsGroup ? environment.strings.Story_Privacy_ChooseCoverGroupInfo : environment.strings.Story_Privacy_ChooseCoverChannelInfo + } + let footerSize = sectionFooter.update( transition: sectionFooterTransition, component: AnyComponent(MultilineTextComponent( @@ -1625,7 +1748,6 @@ final class ShareWithPeersScreenComponent: Component { sectionFooterTransition.setFrame(view: footerView, frame: footerFrame) } } - sectionOffset += section.totalHeight } @@ -1873,6 +1995,38 @@ final class ShareWithPeersScreenComponent: Component { } private var currentHasChannels: Bool? + private var currentHasCover: Bool? + + func saveAndDismiss() { + guard let component = self.component, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else { + return + } + let base: EngineStoryPrivacy.Base + if self.selectedCategories.contains(.everyone) { + base = .everyone + } else if self.selectedCategories.contains(.closeFriends) { + base = .closeFriends + } else if self.selectedCategories.contains(.contacts) { + base = .contacts + } else if self.selectedCategories.contains(.selectedContacts) { + base = .nobody + } else { + base = .nobody + } + component.completion( + self.sendAsPeerId, + EngineStoryPrivacy( + base: base, + additionallyIncludePeers: self.selectedPeers + ), + self.selectedOptions.contains(.screenshot), + self.selectedOptions.contains(.pin), + self.component?.stateContext.stateValue?.peers.filter { self.selectedPeers.contains($0.id) } ?? [], + false + ) + controller.requestDismiss() + } + func update(component: ShareWithPeersScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { guard !self.isDismissed else { return availableSize @@ -1886,6 +2040,7 @@ final class ShareWithPeersScreenComponent: Component { var hasCategories = false var hasChannels = false + var hasCover = false if case .stories = component.stateContext.subject { if let peerId = self.sendAsPeerId, peerId.isGroupOrChannel { } else { @@ -1899,6 +2054,14 @@ final class ShareWithPeersScreenComponent: Component { contentTransition = .spring(duration: 0.4) } self.currentHasChannels = hasChannels + + if self.selectedOptions.contains(.pin) && component.coverItem != nil { + hasCover = true + } + if let currentHasCover = self.currentHasCover, currentHasCover != hasCover { + contentTransition = .spring(duration: 0.4) + } + self.currentHasCover = hasCover } else if case .members = component.stateContext.subject { self.dismissPanGesture?.isEnabled = false } else if case .channels = component.stateContext.subject { @@ -1913,8 +2076,13 @@ final class ShareWithPeersScreenComponent: Component { var sideInset: CGFloat = 0.0 if case .stories = component.stateContext.subject { sideInset = 16.0 - self.scrollView.isScrollEnabled = false - self.dismissPanGesture?.isEnabled = true + if availableSize.width < 393.0 && hasCover { + self.scrollView.isScrollEnabled = true + self.dismissPanGesture?.isEnabled = false + } else { + self.scrollView.isScrollEnabled = false + self.dismissPanGesture?.isEnabled = true + } } else if case .peers = component.stateContext.subject { sideInset = 16.0 self.dismissPanGesture?.isEnabled = false @@ -2287,7 +2455,7 @@ final class ShareWithPeersScreenComponent: Component { if !editing && hasChannels { sections.append(ItemLayout.Section( id: 0, - insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), + insets: UIEdgeInsets(top: component.coverItem == nil ? 28.0 : 12.0, left: 0.0, bottom: 0.0, right: 0.0), itemHeight: peerItemSize.height, itemCount: 1 )) @@ -2306,6 +2474,15 @@ final class ShareWithPeersScreenComponent: Component { itemHeight: optionItemSize.height, itemCount: component.optionItems.count )) + + if hasCover { + sections.append(ItemLayout.Section( + id: 4, + insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), + itemHeight: optionItemSize.height, + itemCount: 1 + )) + } } else { sections.append(ItemLayout.Section( id: 1, @@ -2322,7 +2499,7 @@ final class ShareWithPeersScreenComponent: Component { } else { containerInset += 10.0 } - + var navigationHeight: CGFloat = 56.0 let navigationSideInset: CGFloat = 16.0 var navigationButtonsWidth: CGFloat = 0.0 @@ -2332,33 +2509,10 @@ final class ShareWithPeersScreenComponent: Component { component: AnyComponent(Button( content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), action: { [weak self] in - guard let self, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else { + guard let self else { return } - let base: EngineStoryPrivacy.Base - if self.selectedCategories.contains(.everyone) { - base = .everyone - } else if self.selectedCategories.contains(.closeFriends) { - base = .closeFriends - } else if self.selectedCategories.contains(.contacts) { - base = .contacts - } else if self.selectedCategories.contains(.selectedContacts) { - base = .nobody - } else { - base = .nobody - } - component.completion( - self.sendAsPeerId, - EngineStoryPrivacy( - base: base, - additionallyIncludePeers: self.selectedPeers - ), - self.selectedOptions.contains(.screenshot), - self.selectedOptions.contains(.pin), - self.component?.stateContext.stateValue?.peers.filter { self.selectedPeers.contains($0.id) } ?? [], - false - ) - controller.requestDismiss() + self.saveAndDismiss() } ).minSize(CGSize(width: navigationHeight, height: navigationHeight))), environment: {}, @@ -2460,9 +2614,9 @@ final class ShareWithPeersScreenComponent: Component { } navigationHeight += navigationTextFieldFrame.height - if case .stories = component.stateContext.subject { - navigationHeight += 16.0 - } +// if case .stories = component.stateContext.subject { +// navigationHeight += 16.0 +// } let topInset: CGFloat if environment.inputHeight != 0.0 || !self.navigationTextFieldState.text.isEmpty { @@ -2479,7 +2633,11 @@ final class ShareWithPeersScreenComponent: Component { inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight } else { if !hasCategories { - inset = 314.0 + if self.selectedOptions.contains(.pin) { + inset = 422.0 + } else { + inset = 314.0 + } inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight } else { if hasChannels { @@ -2489,8 +2647,12 @@ final class ShareWithPeersScreenComponent: Component { inset = 1000.0 } } else { - inset = 464.0 - inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight + if self.selectedOptions.contains(.pin) { + inset = 1000.0 + } else { + inset = 464.0 + inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight + } } } } @@ -2758,7 +2920,7 @@ final class ShareWithPeersScreenComponent: Component { bottomPanelInset = 8.0 transition.setFrame(view: self.bottomBackgroundView, frame: CGRect(origin: CGPoint(x: containerSideInset, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: containerWidth, height: bottomPanelHeight + bottomPanelInset))) self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) - transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: containerSideInset + sideInset, y: availableSize.height - bottomPanelHeight - bottomPanelInset - UIScreenPixel), size: CGSize(width: containerWidth, height: UIScreenPixel))) + transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: containerSideInset, y: availableSize.height - bottomPanelHeight - bottomPanelInset - UIScreenPixel), size: CGSize(width: containerWidth, height: UIScreenPixel))) } let itemContainerSize = CGSize(width: itemsContainerWidth, height: availableSize.height) @@ -2768,7 +2930,7 @@ final class ShareWithPeersScreenComponent: Component { contentTransition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: itemLayout.contentHeight + footersTotalHeight))) - let scrollContentHeight = max(topInset + itemLayout.contentHeight + containerInset, availableSize.height - containerInset) + let scrollContentHeight = max(topInset + itemLayout.contentHeight + containerInset + bottomPanelHeight, availableSize.height - containerInset) transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: containerWidth, height: itemLayout.contentHeight))) @@ -2849,10 +3011,12 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { pin: Bool = false, timeout: Int = 0, mentions: [String] = [], + coverImage: UIImage? = nil, stateContext: StateContext, completion: @escaping (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void, editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void = { _, _, _ in }, editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void = { _, _, _ in }, + editCover: @escaping () -> Void = { }, peerCompletion: @escaping (EnginePeer.Id) -> Void = { _ in } ) { self.context = context @@ -2861,6 +3025,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = [] var optionItems: [ShareWithPeersScreenComponent.OptionItem] = [] + var coverItem: ShareWithPeersScreenComponent.CoverItem? if case let .stories(editing) = stateContext.subject { var everyoneSubtitle = presentationData.strings.Story_Privacy_ExcludePeople if (stateContext.stateValue?.savedSelectedPeers[.everyone]?.count ?? 0) > 0 { @@ -2994,6 +3159,10 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { title: presentationData.strings.Story_Privacy_KeepOnMyPage )) } + + if !editing || pin, coverImage != nil { + coverItem = ShareWithPeersScreenComponent.CoverItem(id: .choose, title: presentationData.strings.Story_Privacy_ChooseCover, image: coverImage) + } } var theme: ViewControllerComponentContainer.Theme = .dark @@ -3013,9 +3182,11 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { mentions: mentions, categoryItems: categoryItems, optionItems: optionItems, + coverItem: coverItem, completion: completion, editCategory: editCategory, editBlockedPeers: editBlockedPeers, + editCover: editCover, peerCompletion: peerCompletion ), navigationBarAppearance: .none, theme: theme) diff --git a/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift index 081731cbb3d..5c526ae4b7f 100644 --- a/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift +++ b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift @@ -88,15 +88,17 @@ public final class SliderComponent: Component { if let isTrackingUpdated = component.isTrackingUpdated { internalIsTrackingUpdated = { [weak self] isTracking in if let self { - if isTracking { - self.sliderView?.bordered = true - } else { - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in - self?.sliderView?.bordered = false - }) + if !"".isEmpty { + if isTracking { + self.sliderView?.bordered = true + } else { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in + self?.sliderView?.bordered = false + }) + } } - isTrackingUpdated(isTracking) } + isTrackingUpdated(isTracking) } } diff --git a/submodules/TelegramUI/Components/SpaceWarpView/BUILD b/submodules/TelegramUI/Components/SpaceWarpView/BUILD new file mode 100644 index 00000000000..bfa7fe65eb3 --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SpaceWarpView", + module_name = "SpaceWarpView", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/SpaceWarpView/STCMeshView", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/BUILD b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/BUILD new file mode 100644 index 00000000000..0c1146bbcc1 --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/BUILD @@ -0,0 +1,23 @@ + +objc_library( + name = "STCMeshView", + enable_modules = True, + module_name = "STCMeshView", + srcs = glob([ + "Sources/**/*.m", + "Sources/**/*.h", + ]), + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + sdk_frameworks = [ + "Foundation", + "UIKit", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshLayer.h b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshLayer.h new file mode 100644 index 00000000000..37f984a33a7 --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshLayer.h @@ -0,0 +1,58 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +/* A mesh layer allows individually transforming areas inside its subtree. */ + +@interface STCMeshLayer : CAReplicatorLayer + +/* An array of bounds regions to use for each instance. The length + * of this array is assumed to match `instanceCount'. Required. */ + +@property (atomic, assign) CGRect *instanceBounds; + +/* An array of positions to use for each instance. The length + * of this array is assumed to match `instanceCount'. Required. */ + +@property (atomic, assign) CGPoint *instancePositions; + +/* An array of anchor points to use for each instance. The length + * of this array is assumed to match `instanceCount'. Required. */ + +@property (atomic, assign) CGPoint *instanceAnchorPoints; + +/* An array of transforms to apply to each instance. The length + * of this array is assumed to match `instanceCount'. Required. */ + +@property (atomic, assign) CATransform3D *instanceTransforms; + +/* Add content to this layer to transform it in the mesh. */ + +@property (atomic, strong) CALayer *contentLayer; + +/* This CAReplicatorLayer property is used internally and is not + * available for use by clients. Do not set it. */ + +@property (atomic, assign) CFTimeInterval instanceDelay NS_UNAVAILABLE; + +/* This CAReplicatorLayer property is used internally and is not + * available for use by clients. Do not set it. */ + +@property (atomic, assign) CATransform3D instanceTransform NS_UNAVAILABLE; + +@end + +@interface STCMeshLayer (UIViewSupport) + +/* The wrapper replicator layer used to preserve a linear timespace. */ + +@property (atomic, strong) CAReplicatorLayer *wrapperLayer; + +@end diff --git a/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshView.h b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshView.h new file mode 100644 index 00000000000..7ca8c51bbc9 --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshView.h @@ -0,0 +1,26 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +// shows different part of its subviews with different transforms +@interface STCMeshView : UIView + +@property (nonatomic, retain, readonly) STCMeshLayer *layer; +@property (nonatomic, retain, readwrite) UIView *contentView; // only subviews added to this are transformed + +@property (nonatomic, assign, readwrite) NSInteger instanceCount; // defaults to 1 +@property (nonatomic, assign, readwrite) CATransform3D *instanceTransforms; // optional +@property (nonatomic, assign, readwrite) CGRect *instanceBounds; // optional +@property (nonatomic, assign, readwrite) CGPoint *instancePositions; // optional +@property (nonatomic, assign, readwrite) CGPoint *instanceAnchorPoints; // optional + +@end diff --git a/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshLayer.m b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshLayer.m new file mode 100644 index 00000000000..5d27431f1ff --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshLayer.m @@ -0,0 +1,337 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +static const CFTimeInterval STCMeshLayerTotalInstanceDelay = 10000000.0; +static NSString *const STCMeshLayerBoundsAnimationKey = @"STCMeshLayerBoundsAnimation"; +static NSString *const STCMeshLayerTransformAnimationKey = @"STCMeshLayerTransformAnimation"; +static NSString *const STCMeshLayerPositionAnimationKey = @"STCMeshLayerPositionAnimation"; +static NSString *const STCMeshLayerAnchorPointAnimationKey = @"STCMeshLayerAnchorPointAnimation"; +static NSString *const STCMeshLayerInstanceDelayAnimationKey = @"STCMeshLayerInstanceDelayAnimation"; + +@implementation STCMeshLayer { + CAReplicatorLayer *_wrapperLayer; + CALayer *_contentLayer; + + CGRect *_instanceBounds; + CATransform3D *_instanceTransforms; + CGPoint *_instancePositions; + CGPoint *_instanceAnchorPoints; +} + +#pragma mark - Lifecycle + +- (instancetype)init +{ + if ((self = [super init])) { + self.wrapperLayer = [[CAReplicatorLayer alloc] init]; + self.contentLayer = [[CALayer alloc] init]; + } + + return self; +} + +- (void)dealloc +{ + free(_instanceTransforms); + _instanceTransforms = NULL; + free(_instanceBounds); + _instanceBounds = NULL; +} + +- (void)layoutSublayers +{ + [super layoutSublayers]; + + _wrapperLayer.frame = self.bounds; + _contentLayer.frame = _wrapperLayer.bounds; + [self _updateMeshAnimations]; +} + +#pragma mark - Properties + +@dynamic instanceDelay; + +@dynamic instanceTransform; + +- (void)setInstanceCount:(NSInteger)instanceCount +{ + if (instanceCount != self.instanceCount) { + [super setInstanceCount:instanceCount]; + + free(_instanceTransforms); + _instanceTransforms = NULL; + free(_instanceBounds); + _instanceBounds = NULL; + + [self setNeedsLayout]; + } +} + +- (CATransform3D *)instanceTransforms +{ + CATransform3D *instanceTransforms = _instanceTransforms; + + return instanceTransforms; +} + +- (void)setInstanceTransforms:(CATransform3D *)instanceTransforms +{ + free(_instanceTransforms); + _instanceTransforms = NULL; + + if (instanceTransforms != NULL) { + _instanceTransforms = calloc(sizeof(CATransform3D), self.instanceCount); + memcpy(_instanceTransforms, instanceTransforms, self.instanceCount * sizeof(CATransform3D)); + } + + [self setNeedsLayout]; +} + +- (CGPoint *)instancePositions +{ + CGPoint *instancePositions = _instancePositions; + + return instancePositions; +} + +- (void)setInstancePositions:(CGPoint *)instancePositions +{ + free(_instancePositions); + _instancePositions = NULL; + + if (instancePositions != NULL) { + _instancePositions = calloc(sizeof(CGPoint), self.instanceCount); + memcpy(_instancePositions, instancePositions, self.instanceCount * sizeof(CGPoint)); + } + + [self setNeedsLayout]; +} + +- (CGPoint *)instanceAnchorPoints +{ + CGPoint *instanceAnchorPoints = _instanceAnchorPoints; + + return instanceAnchorPoints; +} + +- (void)setInstanceAnchorPoints:(CGPoint *)instanceAnchorPoints +{ + free(_instanceAnchorPoints); + _instanceAnchorPoints = NULL; + + if (instanceAnchorPoints != NULL) { + _instanceAnchorPoints = calloc(sizeof(CGPoint), self.instanceCount); + memcpy(_instanceAnchorPoints, instanceAnchorPoints, self.instanceCount * sizeof(CGPoint)); + } + + [self setNeedsLayout]; +} + +- (CGRect *)instanceBounds +{ + CGRect *instanceBounds = _instanceBounds; + + return instanceBounds; +} + +- (void)setInstanceBounds:(CGRect *)instanceBounds +{ + free(_instanceBounds); + _instanceBounds = NULL; + + if (instanceBounds != NULL) { + _instanceBounds = calloc(sizeof(CGRect), self.instanceCount); + memcpy(_instanceBounds, instanceBounds, self.instanceCount * sizeof(CGRect)); + } + + [self setNeedsLayout]; +} + +- (CALayer *)contentLayer +{ + CALayer *contentLayer = _contentLayer; + + return contentLayer; +} + +- (void)setContentLayer:(CALayer *)contentLayer +{ + if (contentLayer != _contentLayer) { + if (_contentLayer != nil) { + [_contentLayer removeFromSuperlayer]; + } + + _contentLayer = contentLayer; + + if (_contentLayer != nil) { + [_wrapperLayer addSublayer:_contentLayer]; + } + } +} + +- (CAReplicatorLayer *)wrapperLayer +{ + CAReplicatorLayer *wrapperLayer = _wrapperLayer; + + return wrapperLayer; +} + +- (void)setWrapperLayer:(CAReplicatorLayer *)wrapperLayer +{ + if (wrapperLayer != _wrapperLayer) { + if (_contentLayer != nil) { + [_contentLayer removeFromSuperlayer]; + } + + if (_wrapperLayer != nil) { + [_wrapperLayer removeFromSuperlayer]; + } + + _wrapperLayer = wrapperLayer; + + if (_wrapperLayer != nil) { + _wrapperLayer.masksToBounds = YES; + _wrapperLayer.instanceCount = 2; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGColorRef hiddenColor = CGColorCreate(colorSpace, (CGFloat []){ 1.0, 1.0, 1.0, 0.0 }); + _wrapperLayer.instanceColor = hiddenColor; + CGColorRelease(hiddenColor); + CGColorSpaceRelease(colorSpace); + _wrapperLayer.instanceAlphaOffset = 1.0; + [self addSublayer:_wrapperLayer]; + } + + if (_contentLayer != nil) { + [_wrapperLayer addSublayer:_contentLayer]; + } + + [self setNeedsLayout]; + } +} + +#pragma mark - Internal Methods + +- (CGRect)_boundsAtIndex:(NSUInteger)index +{ + CGRect bounds = CGRectZero; + + if (_instanceBounds != NULL) { + bounds = _instanceBounds[index]; + } + + return bounds; +} + +- (CATransform3D)_transformAtIndex:(NSUInteger)index +{ + CATransform3D transform = CATransform3DIdentity; + + if (_instanceTransforms != NULL) { + transform = _instanceTransforms[index]; + } + + return transform; +} + +- (CGPoint)_positionAtIndex:(NSUInteger)index +{ + CGPoint position = CGPointZero; + + if (_instancePositions != NULL) { + position = _instancePositions[index]; + } + + return position; +} + +- (CGPoint)_anchorPointAtIndex:(NSUInteger)index +{ + CGPoint anchorPoint = CGPointMake(0.0, 0.0); + + if (_instanceAnchorPoints != NULL) { + anchorPoint = _instanceAnchorPoints[index]; + } + + return anchorPoint; +} + +- (void)_updateMeshAnimations +{ + [_wrapperLayer removeAllAnimations]; + + super.instanceDelay = -STCMeshLayerTotalInstanceDelay / self.instanceCount; + + CAKeyframeAnimation *boundsAnimation = [CAKeyframeAnimation animationWithKeyPath:@"bounds"]; + boundsAnimation.calculationMode = kCAAnimationDiscrete; + boundsAnimation.duration = STCMeshLayerTotalInstanceDelay; + boundsAnimation.removedOnCompletion = NO; + NSMutableArray *boundsValues = [NSMutableArray array]; + for (NSUInteger i = 0; i < self.instanceCount; i++) { + CGRect bounds = [self _boundsAtIndex:i]; + NSValue *boundsValue = [NSValue valueWithBytes:&bounds objCType:@encode(CGRect)]; + [boundsValues addObject:boundsValue]; + } + boundsAnimation.values = boundsValues; + [_wrapperLayer addAnimation:boundsAnimation forKey:STCMeshLayerBoundsAnimationKey]; + + CAKeyframeAnimation *transformAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; + transformAnimation.calculationMode = kCAAnimationDiscrete; + transformAnimation.duration = STCMeshLayerTotalInstanceDelay; + transformAnimation.removedOnCompletion = NO; + NSMutableArray *transformValues = [NSMutableArray array]; + for (NSUInteger i = 0; i < self.instanceCount; i++) { + CATransform3D transform = [self _transformAtIndex:i]; + NSValue *transformValue = [NSValue valueWithCATransform3D:transform]; + [transformValues addObject:transformValue]; + } + transformAnimation.values = transformValues; + [_wrapperLayer addAnimation:transformAnimation forKey:STCMeshLayerTransformAnimationKey]; + + CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"]; + positionAnimation.calculationMode = kCAAnimationDiscrete; + positionAnimation.duration = STCMeshLayerTotalInstanceDelay; + positionAnimation.removedOnCompletion = NO; + NSMutableArray *positionValues = [NSMutableArray array]; + for (NSUInteger i = 0; i < self.instanceCount; i++) { + CGPoint position = [self _positionAtIndex:i]; + NSValue *positionValue = [NSValue valueWithBytes:&position objCType:@encode(CGPoint)]; + [positionValues addObject:positionValue]; + } + positionAnimation.values = positionValues; + [_wrapperLayer addAnimation:positionAnimation forKey:STCMeshLayerPositionAnimationKey]; + + CAKeyframeAnimation *anchorPointAnimation = [CAKeyframeAnimation animationWithKeyPath:@"anchorPoint"]; + anchorPointAnimation.calculationMode = kCAAnimationDiscrete; + anchorPointAnimation.duration = STCMeshLayerTotalInstanceDelay; + anchorPointAnimation.removedOnCompletion = NO; + NSMutableArray *anchorPointValues = [NSMutableArray array]; + for (NSUInteger i = 0; i < self.instanceCount; i++) { + CGPoint anchorPoint = [self _anchorPointAtIndex:i]; + NSValue *anchorPointValue = [NSValue valueWithBytes:&anchorPoint objCType:@encode(CGPoint)]; + [anchorPointValues addObject:anchorPointValue]; + } + anchorPointAnimation.values = anchorPointValues; + [_wrapperLayer addAnimation:anchorPointAnimation forKey:STCMeshLayerAnchorPointAnimationKey]; + + CAKeyframeAnimation *timeAnimation = [CAKeyframeAnimation animationWithKeyPath:@"instanceDelay"]; + timeAnimation.calculationMode = kCAAnimationDiscrete; + timeAnimation.duration = STCMeshLayerTotalInstanceDelay; + timeAnimation.removedOnCompletion = NO; + NSMutableArray *timeValues = [NSMutableArray array]; + for (NSUInteger i = 0; i < self.instanceCount; i++) { + CFTimeInterval delay = -super.instanceDelay * i; + [timeValues addObject:@(delay)]; + } + timeAnimation.values = timeValues; + [_wrapperLayer addAnimation:timeAnimation forKey:STCMeshLayerInstanceDelayAnimationKey]; +} + +@end diff --git a/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshView.m b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshView.m new file mode 100644 index 00000000000..b48bffcc1fa --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshView.m @@ -0,0 +1,126 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +@interface _STCMeshViewReplicatorView : UIView + +@property (nonatomic, readonly, retain) CAReplicatorLayer *layer; + +@end + +@implementation _STCMeshViewReplicatorView + +- (CAReplicatorLayer *)layer +{ + return (CAReplicatorLayer *)[super layer]; +} + ++ (Class)layerClass +{ + return [CAReplicatorLayer class]; +} + +@end + +@implementation STCMeshView { + _STCMeshViewReplicatorView *_wrapperView; +} + +- (STCMeshLayer *)layer +{ + return (STCMeshLayer *)[super layer]; +} + ++ (Class)layerClass +{ + return [STCMeshLayer class]; +} + +- (NSInteger)instanceCount +{ + return self.layer.instanceCount; +} + +- (void)setInstanceCount:(NSInteger)instanceCount +{ + self.layer.instanceCount = instanceCount; +} + +- (CATransform3D *)instanceTransforms +{ + return self.layer.instanceTransforms; +} + +- (void)setInstanceTransforms:(CATransform3D *)instanceTransforms +{ + self.layer.instanceTransforms = instanceTransforms; +} + +- (CGRect *)instanceBounds +{ + return self.layer.instanceBounds; +} + +- (void)setInstanceBounds:(CGRect *)instanceBounds +{ + self.layer.instanceBounds = instanceBounds; +} + +- (CGPoint *)instancePositions +{ + return self.layer.instancePositions; +} + +- (void)setInstancePositions:(CGPoint *)instancePositions +{ + self.layer.instancePositions = instancePositions; +} + +- (CGPoint *)instanceAnchorPoints +{ + return self.layer.instanceAnchorPoints; +} + +- (void)setInstanceAnchorPoints:(CGPoint *)instanceAnchorPoints +{ + self.layer.instanceAnchorPoints = instanceAnchorPoints; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) { + _wrapperView = [[_STCMeshViewReplicatorView alloc] init]; + [self addSubview:_wrapperView]; + self.layer.wrapperLayer = _wrapperView.layer; + + self.contentView = [[UIView alloc] init]; + } + + return self; +} + +- (void)setContentView:(UIView *)contentView +{ + if (contentView != _contentView) { + if (_contentView != nil) { + [_contentView removeFromSuperview]; + } + + if (contentView != nil) { + [_wrapperView addSubview:contentView]; + } + + _contentView = contentView; + self.layer.contentLayer = _contentView.layer; + } +} + +@end diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift new file mode 100644 index 00000000000..4d77d7cc97b --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift @@ -0,0 +1,547 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import STCMeshView + +private final class FPSView: UIView { + private var lastTimestamp: Double? + private var counter: Int = 0 + private var fpsValue: Int? + private var fpsString: NSAttributedString? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.anchorPoint = CGPoint() + self.backgroundColor = .black + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update() { + self.counter += 1 + let timestamp = CACurrentMediaTime() + let deltaTime: Double + if let lastTimestamp = self.lastTimestamp { + deltaTime = timestamp - lastTimestamp + } else { + deltaTime = 1.0 / 60.0 + self.lastTimestamp = timestamp + } + if deltaTime >= 1.0 { + let fpsValue = Int(Double(self.counter) / deltaTime) + if self.fpsValue != fpsValue { + self.fpsValue = fpsValue + let fpsString = NSAttributedString(string: "\(fpsValue)", attributes: [.foregroundColor: UIColor.white]) + self.bounds = fpsString.boundingRect(with: CGSize(width: 100.0, height: 100.0), context: nil).integral + self.fpsString = fpsString + self.setNeedsDisplay() + } + self.counter = 0 + self.lastTimestamp = timestamp + } + } + + override func draw(_ rect: CGRect) { + guard let fpsString = self.fpsString else { + return + } + + fpsString.draw(at: CGPoint()) + } +} + +private extension CGPoint { + static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint { + return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) + } + + static func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint { + return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) + } + + static func *(lhs: CGPoint, rhs: CGFloat) -> CGPoint { + return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs) + } +} + +private func length(_ v: CGPoint) -> CGFloat { + return sqrt(v.x * v.x + v.y * v.y) +} + +private func normalize(_ v: CGPoint) -> CGPoint { + let len = length(v) + return CGPoint(x: v.x / len, y: v.y / len) +} + +private struct RippleParams { + var amplitude: CGFloat + var frequency: CGFloat + var decay: CGFloat + var speed: CGFloat + + init(amplitude: CGFloat, frequency: CGFloat, decay: CGFloat, speed: CGFloat) { + self.amplitude = amplitude + self.frequency = frequency + self.decay = decay + self.speed = speed + } +} + +private func rippleOffset( + position: CGPoint, + origin: CGPoint, + time: CGFloat, + params: RippleParams +) -> CGPoint { + // The distance of the current pixel position from `origin`. + let distance: CGFloat = length(position - origin) + + if distance < 1.0 { + return position + } + + // The amount of time it takes for the ripple to arrive at the current pixel position. + let delay = distance / params.speed + + // Adjust for delay, clamp to 0. + var time = time + time -= delay + time = max(0.0, time) + + // The ripple is a sine wave that Metal scales by an exponential decay + // function. + var rippleAmount = params.amplitude * sin(params.frequency * time) * exp(-params.decay * time) + let absRippleAmount = abs(rippleAmount) + if rippleAmount < 0.0 { + rippleAmount = -absRippleAmount + } else { + rippleAmount = absRippleAmount + } + + if distance <= 60.0 { + rippleAmount = 0.4 * rippleAmount + } + + // A vector of length `amplitude` that points away from position. + let n: CGPoint + n = normalize(position - origin) + + // Scale `n` by the ripple amount at the current pixel position and add it + // to the current pixel position. + // + // This new position moves toward or away from `origin` based on the + // sign and magnitude of `rippleAmount`. + return n * (-rippleAmount) +} + +func transformToFitQuad2(frame: CGRect, topLeft tl: CGPoint, topRight tr: CGPoint, bottomLeft bl: CGPoint, bottomRight br: CGPoint) -> (frame: CGRect, transform: CATransform3D) { + let frameTopLeft = frame.origin + + let transform = rectToQuad( + rect: CGRect(origin: CGPoint(), size: frame.size), + quadTL: CGPoint(x: tl.x - frameTopLeft.x, y: tl.y - frameTopLeft.y), + quadTR: CGPoint(x: tr.x - frameTopLeft.x, y: tr.y - frameTopLeft.y), + quadBL: CGPoint(x: bl.x - frameTopLeft.x, y: bl.y - frameTopLeft.y), + quadBR: CGPoint(x: br.x - frameTopLeft.x, y: br.y - frameTopLeft.y) + ) + + let anchorPoint = frame.origin + let anchorOffset = CGPoint(x: anchorPoint.x - frame.origin.x, y: anchorPoint.y - frame.origin.y) + let transPos = CATransform3DMakeTranslation(anchorOffset.x, anchorOffset.y, 0) + let transNeg = CATransform3DMakeTranslation(-anchorOffset.x, -anchorOffset.y, 0) + let fullTransform = CATransform3DConcat(CATransform3DConcat(transPos, transform), transNeg) + + return (frame, fullTransform) +} + +func transformToFitQuad(frame: CGRect, topLeft tl: CGPoint, topRight tr: CGPoint, bottomLeft bl: CGPoint, bottomRight br: CGPoint) -> (frame: CGRect, transform: CATransform3D) { + let boundingBox = boundingBox(forQuadWithTR: tr, tl: tl, bl: bl, br: br) + + let frameTopLeft = boundingBox.origin + let transform = rectToQuad( + rect: CGRect(origin: CGPoint(), size: frame.size), + quadTL: CGPoint(x: tl.x - frameTopLeft.x, y: tl.y - frameTopLeft.y), + quadTR: CGPoint(x: tr.x - frameTopLeft.x, y: tr.y - frameTopLeft.y), + quadBL: CGPoint(x: bl.x - frameTopLeft.x, y: bl.y - frameTopLeft.y), + quadBR: CGPoint(x: br.x - frameTopLeft.x, y: br.y - frameTopLeft.y) + ) + + // To account for anchor point, we must translate, transform, translate + let anchorPoint = frame.center + let anchorOffset = CGPoint(x: anchorPoint.x - boundingBox.origin.x, y: anchorPoint.y - boundingBox.origin.y) + let transPos = CATransform3DMakeTranslation(anchorOffset.x, anchorOffset.y, 0) + let transNeg = CATransform3DMakeTranslation(-anchorOffset.x, -anchorOffset.y, 0) + let fullTransform = CATransform3DConcat(CATransform3DConcat(transPos, transform), transNeg) + + // Now we set our transform + return (boundingBox, fullTransform) +} + +private func boundingBox(forQuadWithTR tr: CGPoint, tl: CGPoint, bl: CGPoint, br: CGPoint) -> CGRect { + var boundingBox = CGRect.zero + + let xmin = min(min(min(tr.x, tl.x), bl.x), br.x) + let ymin = min(min(min(tr.y, tl.y), bl.y), br.y) + let xmax = max(max(max(tr.x, tl.x), bl.x), br.x) + let ymax = max(max(max(tr.y, tl.y), bl.y), br.y) + + boundingBox.origin.x = xmin + boundingBox.origin.y = ymin + boundingBox.size.width = xmax - xmin + boundingBox.size.height = ymax - ymin + + return boundingBox +} + +func rectToQuad(rect: CGRect, quadTL topLeft: CGPoint, quadTR topRight: CGPoint, quadBL bottomLeft: CGPoint, quadBR bottomRight: CGPoint) -> CATransform3D { + return rectToQuad(rect: rect, quadTLX: topLeft.x, quadTLY: topLeft.y, quadTRX: topRight.x, quadTRY: topRight.y, quadBLX: bottomLeft.x, quadBLY: bottomLeft.y, quadBRX: bottomRight.x, quadBRY: bottomRight.y) +} + +private func rectToQuad(rect: CGRect, quadTLX x1a: CGFloat, quadTLY y1a: CGFloat, quadTRX x2a: CGFloat, quadTRY y2a: CGFloat, quadBLX x3a: CGFloat, quadBLY y3a: CGFloat, quadBRX x4a: CGFloat, quadBRY y4a: CGFloat) -> CATransform3D { + let X = rect.origin.x + let Y = rect.origin.y + let W = rect.size.width + let H = rect.size.height + + let y21 = y2a - y1a + let y32 = y3a - y2a + let y43 = y4a - y3a + let y14 = y1a - y4a + let y31 = y3a - y1a + let y42 = y4a - y2a + + let a = -H * (x2a * x3a * y14 + x2a * x4a * y31 - x1a * x4a * y32 + x1a * x3a * y42) + let b = W * (x2a * x3a * y14 + x3a * x4a * y21 + x1a * x4a * y32 + x1a * x2a * y43) + let c = H * X * (x2a * x3a * y14 + x2a * x4a * y31 - x1a * x4a * y32 + x1a * x3a * y42) - H * W * x1a * (x4a * y32 - x3a * y42 + x2a * y43) - W * Y * (x2a * x3a * y14 + x3a * x4a * y21 + x1a * x4a * y32 + x1a * x2a * y43) + + let d = H * (-x4a * y21 * y3a + x2a * y1a * y43 - x1a * y2a * y43 - x3a * y1a * y4a + x3a * y2a * y4a) + let e = W * (x4a * y2a * y31 - x3a * y1a * y42 - x2a * y31 * y4a + x1a * y3a * y42) + let f = -(W * (x4a * (Y * y2a * y31 + H * y1a * y32) - x3a * (H + Y) * y1a * y42 + H * x2a * y1a * y43 + x2a * Y * (y1a - y3a) * y4a + x1a * Y * y3a * (-y2a + y4a)) - H * X * (x4a * y21 * y3a - x2a * y1a * y43 + x3a * (y1a - y2a) * y4a + x1a * y2a * (-y3a + y4a))) + + let g = H * (x3a * y21 - x4a * y21 + (-x1a + x2a) * y43) + let h = W * (-x2a * y31 + x4a * y31 + (x1a - x3a) * y42) + var i = W * Y * (x2a * y31 - x4a * y31 - x1a * y42 + x3a * y42) + H * (X * (-(x3a * y21) + x4a * y21 + x1a * y43 - x2a * y43) + W * (-(x3a * y2a) + x4a * y2a + x2a * y3a - x4a * y3a - x2a * y4a + x3a * y4a)) + + let kEpsilon = 0.0001 + + if abs(i) < kEpsilon { + i = kEpsilon * (i > 0 ? 1.0 : -1.0) + } + + let transform = CATransform3D( + m11: a / i, m12: d / i, m13: 0, m14: g / i, + m21: b / i, m22: e / i, m23: 0, m24: h / i, + m31: 0, m32: 0, m33: 1, m34: 0, + m41: c / i, m42: f / i, m43: 0, m44: 1.0 + ) + + return transform +} + +public protocol SpaceWarpNode: ASDisplayNode { + var contentNode: ASDisplayNode { get } + + func triggerRipple(at point: CGPoint) + func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) +} + +open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { + private final class Shockwave { + let startPoint: CGPoint + var timeValue: CGFloat = 0.0 + + init(startPoint: CGPoint) { + self.startPoint = startPoint + } + } + + public var contentNode: ASDisplayNode { + return self.contentNodeSource + } + + private let contentNodeSource: ASDisplayNode + private let backgroundView: UIView + private var currentCloneView: UIView? + private var meshView: STCMeshView? + + private var gradientLayer: SimpleGradientLayer? + + private var debugLayers: [SimpleLayer] = [] + + #if DEBUG + private var fpsView: FPSView? + #endif + + private var link: SharedDisplayLinkDriver.Link? + + private var shockwaves: [Shockwave] = [] + + private var resolution: (x: Int, y: Int)? + private var layoutParams: (size: CGSize, cornerRadius: CGFloat)? + + override public init() { + self.contentNodeSource = ASDisplayNode() + + self.backgroundView = UIView() + self.backgroundView.backgroundColor = .black + + #if DEBUG && false + self.fpsView = FPSView(frame: CGRect(origin: CGPoint(x: 4.0, y: 40.0), size: CGSize())) + #endif + + super.init() + + self.addSubnode(self.contentNodeSource) + self.view.addSubview(self.backgroundView) + + #if DEBUG + if let fpsView = self.fpsView { + self.view.addSubview(fpsView) + } + #endif + } + + public func triggerRipple(at point: CGPoint) { + 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 + guard let self else { + return + } + for shockwave in self.shockwaves { + shockwave.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor())) + } + + if let (size, cornerRadius) = self.layoutParams { + self.update(size: size, cornerRadius: cornerRadius, transition: .immediate) + } + }) + } + } + + private func updateGrid(resolutionX: Int, resolutionY: Int) { + if let resolution = self.resolution, resolution.x == resolutionX, resolution.y == resolutionY { + return + } + self.resolution = (resolutionX, resolutionY) + + if let meshView = self.meshView { + 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) { + self.layoutParams = (size, cornerRadius) + if size.width <= 0.0 || size.height <= 0.0 { + return + } + + self.contentNodeSource.frame = CGRect(origin: CGPoint(), size: size) + + 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) + + if let currentCloneView = self.currentCloneView { + currentCloneView.removeFromSuperview() + self.currentCloneView = nil + } + + let maxEdge = (max(size.width, size.height) * 0.5) * 2.0 + let maxDistance = sqrt(maxEdge * maxEdge + maxEdge * maxEdge) + let maxDelay = maxDistance / params.speed + + for i in (0 ..< self.shockwaves.count).reversed() { + if self.shockwaves[i].timeValue >= maxDelay { + self.shockwaves.remove(at: i) + } + } + + guard !self.shockwaves.isEmpty else { + if let link = self.link { + self.link = nil + link.invalidate() + } + + if let meshView = self.meshView { + self.meshView = nil + meshView.removeFromSuperview() + } + + for debugLayer in self.debugLayers { + debugLayer.removeFromSuperlayer() + } + self.debugLayers.removeAll() + + self.resolution = nil + self.backgroundView.isHidden = true + self.contentNodeSource.clipsToBounds = false + self.contentNodeSource.layer.cornerRadius = 0.0 + + if let gradientLayer = self.gradientLayer { + self.gradientLayer = nil + gradientLayer.removeFromSuperlayer() + } + + return + } + + 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))) + guard let resolution = self.resolution, let meshView = self.meshView else { + return + } + + if let cloneView = self.contentNodeSource.view.resizableSnapshotView(from: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: false, withCapInsets: UIEdgeInsets()) { + self.currentCloneView = cloneView + meshView.contentView.addSubview(cloneView) + } + + meshView.frame = CGRect(origin: CGPoint(), size: size) + + let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y)) + + var instanceBounds: [CGRect] = [] + var instancePositions: [CGPoint] = [] + var instanceTransforms: [CATransform3D] = [] + + for y in 0 ..< resolution.y { + for x in 0 ..< resolution.x { + let gridPosition = CGPoint(x: CGFloat(x) / CGFloat(resolution.x), y: CGFloat(y) / CGFloat(resolution.y)) + + let sourceRect = CGRect(origin: CGPoint(x: gridPosition.x * (size.width), y: gridPosition.y * (size.height)), size: itemSize) + + let initialTopLeft = CGPoint(x: sourceRect.minX, y: sourceRect.minY) + let initialTopRight = CGPoint(x: sourceRect.maxX, y: sourceRect.minY) + let initialBottomLeft = CGPoint(x: sourceRect.minX, y: sourceRect.maxY) + let initialBottomRight = CGPoint(x: sourceRect.maxX, y: sourceRect.maxY) + + var topLeft = initialTopLeft + var topRight = initialTopRight + var bottomLeft = initialBottomLeft + var bottomRight = initialBottomRight + + for shockwave in self.shockwaves { + topLeft = topLeft + rippleOffset(position: initialTopLeft, origin: shockwave.startPoint, time: shockwave.timeValue, params: params) + topRight = topRight + rippleOffset(position: initialTopRight, origin: shockwave.startPoint, time: shockwave.timeValue, params: params) + bottomLeft = bottomLeft + rippleOffset(position: initialBottomLeft, origin: shockwave.startPoint, time: shockwave.timeValue, params: params) + bottomRight = bottomRight + rippleOffset(position: initialBottomRight, origin: shockwave.startPoint, time: shockwave.timeValue, params: params) + } + /*topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params) + topRight = transformCoordinate(position: topRight, origin: startPoint, time: self.timeValue, params: params) + bottomLeft = transformCoordinate(position: bottomLeft, origin: startPoint, time: self.timeValue, params: params) + bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params)*/ + + let distanceTopLeft = length(topLeft - initialTopLeft) + let distanceTopRight = length(topRight - initialTopRight) + let distanceBottomLeft = length(bottomLeft - initialBottomLeft) + let distanceBottomRight = length(bottomRight - initialBottomRight) + var maxDistance = max(distanceTopLeft, distanceTopRight) + maxDistance = max(maxDistance, distanceBottomLeft) + maxDistance = max(maxDistance, distanceBottomRight) + + var (frame, transform) = transformToFitQuad2(frame: sourceRect, topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight) + + if maxDistance <= 0.005 { + transform = CATransform3DIdentity + } + + instanceBounds.append(frame) + instancePositions.append(frame.origin) + + instanceTransforms.append(transform) + } + } + + instanceBounds.withUnsafeMutableBufferPointer { buffer in + meshView.instanceBounds = buffer.baseAddress! + } + instancePositions.withUnsafeMutableBufferPointer { buffer in + meshView.instancePositions = buffer.baseAddress! + } + instanceTransforms.withUnsafeMutableBufferPointer { buffer in + 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] + } + } + + override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.alpha.isZero || self.isHidden || !self.isUserInteractionEnabled { + return nil + } + for view in self.contentNode.view.subviews.reversed() { + if let result = view.hitTest(self.view.convert(point, to: view), with: event), result.isUserInteractionEnabled { + return result + } + } + + let result = super.hitTest(point, with: event) + if result != self { + return result + } else { + return nil + } + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift index 545bfc84095..679bb8df662 100644 --- a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift @@ -65,7 +65,7 @@ public final class StarsAvatarComponent: Component { private weak var state: EmptyComponentState? override init(frame: CGRect) { - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0)) + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 20.0)) super.init(frame: frame) diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD b/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD index 99fda0c389c..b71d4ed3c09 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD @@ -22,6 +22,8 @@ swift_library( "//submodules/AvatarNode", "//submodules/AccountContext", "//submodules/InvisibleInkDustNode", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index 3e6c5fc93d6..6c1e5dc9484 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift @@ -11,6 +11,8 @@ import PhotoResources import AvatarNode import AccountContext import InvisibleInkDustNode +import AnimatedStickerNode +import TelegramAnimatedStickerNode final class StarsParticlesView: UIView { private struct Particle { @@ -248,9 +250,10 @@ public final class StarsImageComponent: Component { public enum Subject: Equatable { case none case photo(TelegramMediaWebFile) - case media([Media]) + case media([AnyMediaReference]) case extendedMedia([TelegramExtendedMedia]) case transactionPeer(StarsContext.State.Transaction.Peer) + case gift(Int64) public static func == (lhs: StarsImageComponent.Subject, rhs: StarsImageComponent.Subject) -> Bool { switch lhs { @@ -267,7 +270,7 @@ public final class StarsImageComponent: Component { return false } case let .media(lhsMedia): - if case let .media(rhsMedia) = rhs, areMediaArraysEqual(lhsMedia, rhsMedia) { + if case let .media(rhsMedia) = rhs, areMediaArraysEqual(lhsMedia.map { $0.media }, rhsMedia.map { $0.media }) { return true } else { return false @@ -284,6 +287,12 @@ public final class StarsImageComponent: Component { } else { return false } + case let .gift(lhsCount): + if case let .gift(rhsCount) = rhs, lhsCount == rhsCount { + return true + } else { + return false + } } } } @@ -347,6 +356,8 @@ public final class StarsImageComponent: Component { private var dustNode: MediaDustNode? private var button: UIControl? + private var animationNode: AnimatedStickerNode? + private var lockView: UIImageView? private var countView = ComponentView() @@ -388,7 +399,7 @@ public final class StarsImageComponent: Component { guard let component = self.component, let containerNode = self.containerNode else { return nil } - if case let .media(media) = component.subject, media.first?.id == transitionMedia.id { + if case let .media(media) = component.subject, media.first?.media.id == transitionMedia.id { return (containerNode, containerNode.bounds, { [weak containerNode] in return (containerNode?.view.snapshotContentTree(unhide: true), nil) }) @@ -477,25 +488,25 @@ public final class StarsImageComponent: Component { containerNode.view.addSubview(imageNode.view) self.imageNode = imageNode } - if let image = media.first as? TelegramMediaImage { - if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions { + if let imageReference = media.first?.concrete(TelegramMediaImage.self) { + if let imageDimensions = largestImageRepresentation(imageReference.media.representations)?.dimensions { dimensions = imageDimensions.cgSize.aspectFilled(imageSize) } if isFirstTime { - imageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false)) + imageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: imageReference, onlyFullSize: false, blurred: false)) } - } else if let file = media.first as? TelegramMediaFile { - if let videoDimensions = file.dimensions { + } else if let fileReference = media.first?.concrete(TelegramMediaFile.self) { + if let videoDimensions = fileReference.media.dimensions { dimensions = videoDimensions.cgSize.aspectFilled(imageSize) } if isFirstTime { - imageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), useLargeThumbnail: true, autoFetchFullSizeThumbnail: true)) + imageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: fileReference, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true)) } } imageNode.frame = imageFrame imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: dimensions, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() - if let firstMedia = media.first, self.hiddenMedia.contains(where: { $0.id == firstMedia.id }) { + if let firstMedia = media.first?.media, self.hiddenMedia.contains(where: { $0.id == firstMedia.id }) { containerNode.isHidden = true } else { containerNode.isHidden = false @@ -520,19 +531,19 @@ public final class StarsImageComponent: Component { self.imageFrameNode = imageFrameNode } - if let image = media[1] as? TelegramMediaImage { - if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions { + if let imageReference = media[1].concrete(TelegramMediaImage.self) { + if let imageDimensions = largestImageRepresentation(imageReference.media.representations)?.dimensions { secondDimensions = imageDimensions.cgSize.aspectFilled(imageSize) } if isFirstTime { - secondImageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false)) + secondImageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: imageReference, onlyFullSize: false, blurred: false)) } - } else if let file = media[1] as? TelegramMediaFile { - if let videoDimensions = file.dimensions { + } else if let fileReference = media[1].concrete(TelegramMediaFile.self) { + if let videoDimensions = fileReference.media.dimensions { secondDimensions = videoDimensions.cgSize.aspectFilled(imageSize) } if isFirstTime { - secondImageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), useLargeThumbnail: true, autoFetchFullSizeThumbnail: true)) + secondImageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: fileReference, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true)) } } @@ -776,6 +787,31 @@ public final class StarsImageComponent: Component { iconBackgroundView.frame = imageFrame iconView.frame = imageFrame.insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset) } + case let .gift(count): + let animationNode: AnimatedStickerNode + if let current = self.animationNode { + animationNode = current + } else { + let stickerName: String + if count <= 1000 { + stickerName = "Gift3" + } else if count < 2500 { + stickerName = "Gift6" + } else { + stickerName = "Gift12" + } + animationNode = DefaultAnimatedStickerNodeImpl() + animationNode.autoplay = true + animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: stickerName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil)) + animationNode.visibility = true + containerNode.view.addSubview(animationNode.view) + self.animationNode = animationNode + + animationNode.playOnce() + } + let animationFrame = imageFrame.insetBy(dx: -imageFrame.width * 0.19, dy: -imageFrame.height * 0.19).offsetBy(dx: 0.0, dy: -14.0) + animationNode.frame = animationFrame + animationNode.updateLayout(size: animationFrame.size) } if let _ = component.action { diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index 3808503034e..a5257985bd5 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -27,9 +27,32 @@ import BundleIconComponent import ConfettiEffect private struct StarsProduct: Equatable { - let option: StarsTopUpOption + enum Option: Equatable { + case topUp(StarsTopUpOption) + case gift(StarsGiftOption) + } + + let option: Option let storeProduct: InAppPurchaseManager.Product + var count: Int64 { + switch self.option { + case let .topUp(option): + return option.count + case let .gift(option): + return option.count + } + } + + var isExtended: Bool { + switch self.option { + case let .topUp(option): + return option.isExtended + case let .gift(option): + return option.isExtended + } + } + var id: String { return self.storeProduct.id } @@ -54,44 +77,47 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { let externalState: ExternalState let containerSize: CGSize let balance: Int64? - let options: [StarsTopUpOption] - let peerId: EnginePeer.Id? - let requiredStars: Int64? + let options: [Any] + let purpose: StarsPurchasePurpose let selectedProductId: String? let forceDark: Bool let products: [StarsProduct]? let expanded: Bool + let peers: [EnginePeer.Id: EnginePeer] let stateUpdated: (ComponentTransition) -> Void let buy: (StarsProduct) -> Void + let openAppExamples: () -> Void init( context: AccountContext, externalState: ExternalState, containerSize: CGSize, balance: Int64?, - options: [StarsTopUpOption], - peerId: EnginePeer.Id?, - requiredStars: Int64?, + options: [Any], + purpose: StarsPurchasePurpose, selectedProductId: String?, forceDark: Bool, products: [StarsProduct]?, expanded: Bool, + peers: [EnginePeer.Id: EnginePeer], stateUpdated: @escaping (ComponentTransition) -> Void, - buy: @escaping (StarsProduct) -> Void + buy: @escaping (StarsProduct) -> Void, + openAppExamples: @escaping () -> Void ) { self.context = context self.externalState = externalState self.containerSize = containerSize self.balance = balance self.options = options - self.peerId = peerId - self.requiredStars = requiredStars + self.purpose = purpose self.selectedProductId = selectedProductId self.forceDark = forceDark self.products = products self.expanded = expanded + self.peers = peers self.stateUpdated = stateUpdated self.buy = buy + self.openAppExamples = openAppExamples } static func ==(lhs: StarsPurchaseScreenContentComponent, rhs: StarsPurchaseScreenContentComponent) -> Bool { @@ -101,13 +127,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { if lhs.containerSize != rhs.containerSize { return false } - if lhs.options != rhs.options { - return false - } - if lhs.peerId != rhs.peerId { - return false - } - if lhs.requiredStars != rhs.requiredStars { + if lhs.purpose != rhs.purpose { return false } if lhs.selectedProductId != rhs.selectedProductId { @@ -122,6 +142,9 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { if lhs.expanded != rhs.expanded { return false } + if lhs.peers != rhs.peers { + return false + } return true } @@ -129,31 +152,18 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { private let context: AccountContext var products: [StarsProduct]? - var peer: EnginePeer? private var disposable: Disposable? - + + var cachedChevronImage: (UIImage, PresentationTheme)? + init( context: AccountContext, - peerId: EnginePeer.Id? + purpose: StarsPurchasePurpose ) { self.context = context super.init() - - if let peerId { - self.disposable = (context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) - ) - |> deliverOnMainQueue).start(next: { [weak self] peer in - if let self, let peer { - self.peer = peer - self.updated(transition: .immediate) - } - }) - } - - let _ = updatePremiumPromoConfigurationOnce(account: context.account).start() } deinit { @@ -162,63 +172,32 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, peerId: self.peerId) + return State(context: self.context, purpose: self.purpose) } static var body: Body { -// let overscroll = Child(Rectangle.self) -// let fade = Child(RoundedRectangle.self) let text = Child(BalancedTextComponent.self) let list = Child(VStack.self) let termsText = Child(BalancedTextComponent.self) return { context in let sideInset: CGFloat = 16.0 - + + let component = context.component let scrollEnvironment = context.environment[ScrollChildEnvironment.self].value let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let state = context.state - state.products = context.component.products + + state.products = component.products let theme = environment.theme let strings = environment.strings - let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let availableWidth = context.availableSize.width let sideInsets = sideInset * 2.0 + environment.safeInsets.left + environment.safeInsets.right var size = CGSize(width: context.availableSize.width, height: 0.0) - -// var topBackgroundColor = theme.list.plainBackgroundColor -// let bottomBackgroundColor = theme.list.blocksBackgroundColor -// if theme.overallDarkAppearance { -// topBackgroundColor = bottomBackgroundColor -// } -// -// let overscroll = overscroll.update( -// component: Rectangle(color: topBackgroundColor), -// availableSize: CGSize(width: context.availableSize.width, height: 1000), -// transition: context.transition -// ) -// context.add(overscroll -// .position(CGPoint(x: overscroll.size.width / 2.0, y: -overscroll.size.height / 2.0)) -// ) -// -// let fade = fade.update( -// component: RoundedRectangle( -// colors: [ -// topBackgroundColor, -// bottomBackgroundColor -// ], -// cornerRadius: 0.0, -// gradientDirection: .vertical -// ), -// availableSize: CGSize(width: availableWidth, height: 300), -// transition: context.transition -// ) -// context.add(fade -// .position(CGPoint(x: fade.size.width / 2.0, y: fade.size.height / 2.0)) -// ) - + size.height += 183.0 + 10.0 + environment.navigationHeight - 56.0 let textColor = theme.list.itemPrimaryTextColor @@ -228,22 +207,38 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { let boldTextFont = Font.semibold(15.0) let textString: String - if let _ = context.component.requiredStars { - textString = state.peer == nil ? strings.Stars_Purchase_StarsNeededUnlockInfo : strings.Stars_Purchase_StarsNeededInfo(state.peer?.compactDisplayTitle ?? "").string - } else { + switch context.component.purpose { + case let .generic(requiredStars): + let _ = requiredStars textString = strings.Stars_Purchase_GetStarsInfo + case .gift: + textString = strings.Stars_Purchase_GiftInfo(component.peers.first?.value.compactDisplayTitle ?? "").string + case .transfer: + textString = strings.Stars_Purchase_StarsNeededInfo(component.peers.first?.value.compactDisplayTitle ?? "").string + case let .subscription(_, _, renew): + textString = renew ? strings.Stars_Purchase_SubscriptionRenewInfo(component.peers.first?.value.compactDisplayTitle ?? "").string : strings.Stars_Purchase_SubscriptionInfo(component.peers.first?.value.compactDisplayTitle ?? "").string + case .unlockMedia: + textString = strings.Stars_Purchase_StarsNeededUnlockInfo } let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== theme { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: accentColor)!, theme) + } + + let textAttributedString = parseMarkdownIntoAttributedString(textString, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString + + if let range = textAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + textAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: textAttributedString.string)) + } + + let openAppExamples = component.openAppExamples let text = text.update( component: BalancedTextComponent( - text: .markdown( - text: textString, - attributes: markdownAttributes - ), + text: .plain(textAttributedString), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2, @@ -256,6 +251,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { } }, tapAction: { _, _ in + openAppExamples() } ), environment: {}, @@ -271,16 +267,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { size.height += 21.0 context.component.externalState.descriptionHeight = text.size.height - - let initialValues: [Int64] = [ - 15, - 75, - 250, - 500, - 1000, - 2500 - ] - + let stars: [Int64: Int] = [ 15: 1, 75: 2, @@ -288,13 +275,19 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { 500: 4, 1000: 5, 2500: 6, + 25: 1, 50: 1, 100: 2, 150: 2, 350: 3, 750: 4, - 1500: 5 + 1500: 5, + + 5000: 6, + 10000: 6, + 25000: 7, + 35000: 7 ] let externalStateUpdated = context.component.stateUpdated @@ -306,21 +299,26 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { if let products = state.products, let balance = context.component.balance { var minimumCount: Int64? - if let requiredStars = context.component.requiredStars { - minimumCount = requiredStars - balance + if let requiredStars = context.component.purpose.requiredStars { + if case .generic = context.component.purpose { + minimumCount = requiredStars + } else { + minimumCount = requiredStars - balance + } } + for product in products { - if let minimumCount, minimumCount > product.option.count && !(items.isEmpty && product.id == products.last?.id) { + if let minimumCount, minimumCount > product.count && !(items.isEmpty && product.id == products.last?.id) { continue } if let _ = minimumCount, items.isEmpty { - } else if !context.component.expanded && !initialValues.contains(product.option.count) { + } else if !context.component.expanded && product.isExtended { continue } - let title = strings.Stars_Purchase_Stars(Int32(product.option.count)) + let title = strings.Stars_Purchase_Stars(Int32(product.count)) let price = product.price let titleComponent = AnyComponent(MultilineTextComponent( @@ -354,7 +352,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { title: titleComponent, contentInsets: UIEdgeInsets(top: 12.0, left: -6.0, bottom: 12.0, right: 0.0), leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(StarsIconComponent( - count: stars[product.option.count] ?? 1 + count: stars[product.count] ?? 1 ))), true), accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( @@ -439,7 +437,6 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { }) let textSideInset: CGFloat = 16.0 - let component = context.component let termsText = termsText.update( component: BalancedTextComponent( text: .markdown(text: strings.Stars_Purchase_Info, attributes: termsMarkdownAttributes), @@ -484,10 +481,10 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { let context: AccountContext let starsContext: StarsContext - let options: [StarsTopUpOption] - let peerId: EnginePeer.Id? - let requiredStars: Int64? + let options: [Any] + let purpose: StarsPurchasePurpose let forceDark: Bool + let openAppExamples: () -> Void let updateInProgress: (Bool) -> Void let present: (ViewController) -> Void let completion: (Int64) -> Void @@ -495,10 +492,10 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { init( context: AccountContext, starsContext: StarsContext, - options: [StarsTopUpOption], - peerId: EnginePeer.Id?, - requiredStars: Int64?, + options: [Any], + purpose: StarsPurchasePurpose, forceDark: Bool, + openAppExamples: @escaping () -> Void, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping (Int64) -> Void @@ -506,9 +503,9 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { self.context = context self.starsContext = starsContext self.options = options - self.peerId = peerId - self.requiredStars = requiredStars + self.purpose = purpose self.forceDark = forceDark + self.openAppExamples = openAppExamples self.updateInProgress = updateInProgress self.present = present self.completion = completion @@ -521,13 +518,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { if lhs.starsContext !== rhs.starsContext { return false } - if lhs.options != rhs.options { - return false - } - if lhs.peerId != rhs.peerId { - return false - } - if lhs.requiredStars != rhs.requiredStars { + if lhs.purpose != rhs.purpose { return false } if lhs.forceDark != rhs.forceDark { @@ -538,6 +529,8 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { final class State: ComponentState { private let context: AccountContext + private let purpose: StarsPurchasePurpose + private let updateInProgress: (Bool) -> Void private let present: (ViewController) -> Void private let completion: (Int64) -> Void @@ -548,11 +541,11 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { var hasIdleAnimations = true var progressProduct: StarsProduct? - - private(set) var promoConfiguration: PremiumPromoConfiguration? - + private(set) var products: [StarsProduct]? private(set) var starsState: StarsContext.State? + + var peers: [EnginePeer.Id: EnginePeer] = [:] let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer @@ -563,12 +556,14 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { init( context: AccountContext, starsContext: StarsContext, - initialOptions: [StarsTopUpOption], + purpose: StarsPurchasePurpose, + initialOptions: [Any], updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping (Int64) -> Void ) { self.context = context + self.purpose = purpose self.updateInProgress = updateInProgress self.present = present self.completion = completion @@ -584,32 +579,65 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { } else { availableProducts = .single([]) } - - let options: Signal<[StarsTopUpOption], NoError> - if !initialOptions.isEmpty { - options = .single(initialOptions) - } else { - options = .single([]) |> then(context.engine.payments.starsTopUpOptions()) + + let products: Signal<[StarsProduct], NoError> + switch purpose { + case .gift: + let options: Signal<[StarsGiftOption], NoError> + if !initialOptions.isEmpty, let initialGiftOptions = initialOptions as? [StarsGiftOption] { + options = .single(initialGiftOptions) + } else { + options = .single([]) |> then(context.engine.payments.starsGiftOptions(peerId: nil)) + } + products = combineLatest(availableProducts, options) + |> map { availableProducts, options in + var products: [StarsProduct] = [] + for option in options { + if let product = availableProducts.first(where: { $0.id == option.storeProductId }) { + products.append(StarsProduct(option: .gift(option), storeProduct: product)) + } + } + return products + } + default: + let options: Signal<[StarsTopUpOption], NoError> + if !initialOptions.isEmpty, let initialTopUpOptions = initialOptions as? [StarsTopUpOption] { + options = .single(initialTopUpOptions) + } else { + options = .single([]) |> then(context.engine.payments.starsTopUpOptions()) + } + products = combineLatest(availableProducts, options) + |> map { availableProducts, options in + var products: [StarsProduct] = [] + for option in options { + if let product = availableProducts.first(where: { $0.id == option.storeProductId }) { + products.append(StarsProduct(option: .topUp(option), storeProduct: product)) + } + } + return products + } } - + + let peerIds = purpose.peerIds self.disposable = combineLatest( queue: Queue.mainQueue(), - availableProducts, - options, - starsContext.state - ).start(next: { [weak self] availableProducts, options, starsState in + products, + starsContext.state, + context.engine.data.get(EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))) + ).start(next: { [weak self] products, starsState, result in guard let self else { return } - var products: [StarsProduct] = [] - for option in options { - if let product = availableProducts.first(where: { $0.id == option.storeProductId }) { - products.append(StarsProduct(option: option, storeProduct: product)) + self.products = products.sorted(by: { $0.count < $1.count }) + self.starsState = starsState + + var peers: [EnginePeer.Id: EnginePeer] = [:] + for peerId in peerIds { + if let maybePeer = result[peerId], let peer = maybePeer { + peers[peerId] = peer } } - - self.products = products.sorted(by: { $0.option.count < $1.option.count }) - self.starsState = starsState + self.peers = peers self.updated(transition: .immediate) }) @@ -630,7 +658,13 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { self.updated(transition: .easeInOut(duration: 0.2)) let (currency, amount) = product.storeProduct.priceCurrencyAndAmount - let purpose: AppStoreTransactionPurpose = .stars(count: product.option.count, currency: currency, amount: amount) + let purpose: AppStoreTransactionPurpose + switch self.purpose { + case let .gift(peerId): + purpose = .starsGift(peerId: peerId, count: product.count, currency: currency, amount: amount) + default: + purpose = .stars(count: product.count, currency: currency, amount: amount) + } let _ = (self.context.engine.payments.canPurchasePremium(purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] available in @@ -643,7 +677,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { self.updateInProgress(false) self.updated(transition: .easeInOut(duration: 0.2)) - self.completion(product.option.count) + self.completion(product.count) } }, error: { [weak self] error in if let strongSelf = self { @@ -693,13 +727,14 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, starsContext: self.starsContext, initialOptions: self.options, updateInProgress: self.updateInProgress, present: self.present, completion: self.completion) + return State(context: self.context, starsContext: self.starsContext, purpose: self.purpose, initialOptions: self.options, updateInProgress: self.updateInProgress, present: self.present, completion: self.completion) } static var body: Body { let background = Child(Rectangle.self) let scrollContent = Child(ScrollComponent.self) let star = Child(PremiumStarComponent.self) + let avatar = Child(GiftAvatarComponent.self) let topPanel = Child(BlurredBackgroundComponent.self) let topSeparator = Child(Rectangle.self) let title = Child(MultilineTextComponent.self) @@ -724,23 +759,44 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { starIsVisible = false } - let header = star.update( - component: PremiumStarComponent( - theme: environment.theme, - isIntro: true, - isVisible: starIsVisible, - hasIdleAnimations: state.hasIdleAnimations, - colors: [ - UIColor(rgb: 0xe57d02), - UIColor(rgb: 0xf09903), - UIColor(rgb: 0xf9b004), - UIColor(rgb: 0xfdd219) - ], - particleColor: UIColor(rgb: 0xf9b004) - ), - availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), - transition: context.transition - ) + let header: _UpdatedChildComponent + if case let .gift(peerId) = context.component.purpose { + var peers: [EnginePeer] = [] + if let peer = state.peers[peerId] { + peers.append(peer) + } + header = avatar.update( + component: GiftAvatarComponent( + context: context.component.context, + theme: environment.theme, + peers: peers, + isVisible: starIsVisible, + hasIdleAnimations: state.hasIdleAnimations, + color: UIColor(rgb: 0xf9b004), + hasLargeParticles: true + ), + availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), + transition: context.transition + ) + } else { + header = star.update( + component: PremiumStarComponent( + theme: environment.theme, + isIntro: true, + isVisible: starIsVisible, + hasIdleAnimations: state.hasIdleAnimations, + colors: [ + UIColor(rgb: 0xe57d02), + UIColor(rgb: 0xf09903), + UIColor(rgb: 0xf9b004), + UIColor(rgb: 0xfdd219) + ], + particleColor: UIColor(rgb: 0xf9b004) + ), + availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), + transition: context.transition + ) + } let topPanel = topPanel.update( component: BlurredBackgroundComponent( @@ -759,10 +815,17 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { ) let titleText: String - if let requiredStars = context.component.requiredStars { + 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 .gift: + titleText = strings.Stars_Purchase_GiftStars + case let .transfer(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars): titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) - } else { - titleText = strings.Stars_Purchase_GetStars } let title = title.update( @@ -814,12 +877,12 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { containerSize: context.availableSize, balance: state.starsState?.balance, options: context.component.options, - peerId: context.component.peerId, - requiredStars: context.component.requiredStars, + purpose: context.component.purpose, selectedProductId: state.progressProduct?.storeProduct.id, forceDark: context.component.forceDark, products: state.products, expanded: state.isExpanded, + peers: state.peers, stateUpdated: { [weak state] transition in scrollAction.invoke(CGPoint(x: 0.0, y: 150.0 + contentExternalState.descriptionHeight)) state?.isExpanded = true @@ -827,7 +890,8 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { }, buy: { [weak state] product in state?.buy(product: product) - } + }, + openAppExamples: context.component.openAppExamples )), contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: environment.safeInsets.bottom, right: 0.0), contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in @@ -923,7 +987,6 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { public final class StarsPurchaseScreen: ViewControllerComponentContainer { fileprivate let context: AccountContext fileprivate let starsContext: StarsContext - fileprivate let options: [StarsTopUpOption] private var didSetReady = false private let _ready = Promise() @@ -934,17 +997,14 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { public init( context: AccountContext, starsContext: StarsContext, - options: [StarsTopUpOption], - peerId: EnginePeer.Id?, - requiredStars: Int64?, - modal: Bool = true, - forceDark: Bool = false, + options: [Any] = [], + purpose: StarsPurchasePurpose, completion: @escaping (Int64) -> Void = { _ in } ) { self.context = context self.starsContext = starsContext - self.options = options + var openAppExamplesImpl: (() -> Void)? var updateInProgressImpl: ((Bool) -> Void)? var presentImpl: ((ViewController) -> Void)? var completionImpl: ((Int64) -> Void)? @@ -952,9 +1012,11 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { context: context, starsContext: starsContext, options: options, - peerId: peerId, - requiredStars: requiredStars, - forceDark: forceDark, + purpose: purpose, + forceDark: false, + openAppExamples: { + openAppExamplesImpl?() + }, updateInProgress: { inProgress in updateInProgressImpl?(inProgress) }, @@ -964,16 +1026,25 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { completion: { stars in completionImpl?(stars) } - ), navigationBarAppearance: .transparent, presentationMode: modal ? .modal : .default, theme: forceDark ? .dark : .default) + ), navigationBarAppearance: .transparent, presentationMode: .modal, theme: .default) let presentationData = context.sharedContext.currentPresentationData.with { $0 } - if modal { - let cancelItem = UIBarButtonItem(title: presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) - self.navigationItem.setLeftBarButton(cancelItem, animated: false) - self.navigationPresentation = .modal - } else { - self.navigationPresentation = .modalInLargeLayout + let cancelItem = UIBarButtonItem(title: presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.setLeftBarButton(cancelItem, animated: false) + self.navigationPresentation = .modal + + openAppExamplesImpl = { [weak self] in + guard let self else { + return + } + let _ = (context.sharedContext.makeMiniAppListScreenInitialData(context: context) + |> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + navigationController.pushViewController(context.sharedContext.makeMiniAppListScreen(context: context, initialData: initialData)) + }) } updateInProgressImpl = { [weak self] inProgress in @@ -1037,6 +1108,9 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { if let view = self.node.hostView.findTaggedView(tag: PremiumStarComponent.View.Tag()) as? PremiumStarComponent.View { self.didSetReady = true self._ready.set(view.ready) + } else if let view = self.node.hostView.findTaggedView(tag: GiftAvatarComponent.View.Tag()) as? GiftAvatarComponent.View { + self.didSetReady = true + self._ready.set(view.ready) } } } @@ -1135,3 +1209,33 @@ final class StarsIconComponent: CombinedComponent { } } } + +private extension StarsPurchasePurpose { + var peerIds: [EnginePeer.Id] { + switch self { + case let .gift(peerId): + return [peerId] + case let .transfer(peerId, _): + return [peerId] + case let .subscription(peerId, _, _): + return [peerId] + default: + return [] + } + } + + var requiredStars: Int64? { + switch self { + case let .generic(requiredStars): + return requiredStars + case let .transfer(_, requiredStars): + return requiredStars + case let .subscription(_, requiredStars, _): + return requiredStars + case let .unlockMedia(requiredStars): + return requiredStars + default: + return nil + } + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD index f392b1ca5e9..62a8ef68a8f 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD @@ -32,7 +32,9 @@ swift_library( "//submodules/Components/SolidRoundedButtonComponent", "//submodules/AvatarNode", "//submodules/TelegramUI/Components/Stars/StarsImageComponent", + "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", "//submodules/GalleryUI", + "//submodules/TelegramUI/Components/MiniAppListScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index b73fa35bdbd..ee53a897b4a 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -22,6 +22,8 @@ import TelegramStringFormatting import UndoUI import StarsImageComponent import GalleryUI +import StarsAvatarComponent +import MiniAppListScreen private final class StarsTransactionSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -33,6 +35,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { 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 init( @@ -43,6 +46,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { 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 ) { self.context = context @@ -52,6 +56,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { self.openPeer = openPeer self.openMessage = openMessage self.openMedia = openMedia + self.openAppExamples = openAppExamples self.copyTransactionId = copyTransactionId } @@ -73,6 +78,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { var peerMap: [EnginePeer.Id: EnginePeer] = [:] var cachedCloseImage: (UIImage, PresentationTheme)? + var cachedChevronImage: (UIImage, PresentationTheme)? var inProgress = false @@ -89,6 +95,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { } case let .receipt(receipt): peerIds.append(receipt.botPaymentId) + case let .gift(message): + peerIds.append(message.id.peerId) } self.disposable = (context.engine.data.get( @@ -136,6 +144,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { let refundBackgound = Child(RoundedRectangle.self) let refundText = Child(MultilineTextComponent.self) + let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: []) + return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let controller = environment.controller @@ -172,101 +182,126 @@ private final class StarsTransactionSheetContent: CombinedComponent { let titleText: String let amountText: String - let descriptionText: String + var descriptionText: String let additionalText: String let buttonText: String let count: Int64 + var countIsGeneric = false + var countOnTop = false let transactionId: String? let date: Int32 let via: String? let messageId: EngineMessage.Id? let toPeer: EnginePeer? let transactionPeer: StarsContext.State.Transaction.Peer? - let media: [Media] + let media: [AnyMediaReference] let photo: TelegramMediaWebFile? let isRefund: Bool + let isGift: Bool var delayedCloseOnOpenPeer = true switch subject { case let .transaction(transaction, parentPeer): - switch transaction.peer { - case let .peer(peer): - if !transaction.media.isEmpty { - titleText = strings.Stars_Transaction_MediaPurchase - } else { - titleText = transaction.title ?? peer.compactDisplayTitle - } + 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 - case .appStore: - titleText = strings.Stars_Transaction_AppleTopUp_Title - via = strings.Stars_Transaction_AppleTopUp_Subtitle - case .playMarket: - titleText = strings.Stars_Transaction_GoogleTopUp_Title - via = strings.Stars_Transaction_GoogleTopUp_Subtitle - case .premiumBot: - titleText = strings.Stars_Transaction_PremiumBotTopUp_Title - via = strings.Stars_Transaction_PremiumBotTopUp_Subtitle - case .fragment: - if parentPeer.id == component.context.account.peerId { - titleText = strings.Stars_Transaction_FragmentTopUp_Title - via = strings.Stars_Transaction_FragmentTopUp_Subtitle + messageId = nil + date = transaction.date + if case let .peer(peer) = transaction.peer { + toPeer = peer } else { - titleText = strings.Stars_Transaction_FragmentWithdrawal_Title - via = strings.Stars_Transaction_FragmentWithdrawal_Subtitle + toPeer = nil } - case .ads: - titleText = strings.Stars_Transaction_TelegramAds_Title - via = strings.Stars_Transaction_TelegramAds_Subtitle - case .unsupported: - titleText = strings.Stars_Transaction_Unsupported_Title - via = nil - } - if !transaction.media.isEmpty { - var description: String = "" - var photoCount: Int32 = 0 - var videoCount: Int32 = 0 - for media in transaction.media { - if let _ = media as? TelegramMediaFile { - videoCount += 1 + transactionPeer = transaction.peer + media = [] + photo = nil + isRefund = false + isGift = true + } else { + switch transaction.peer { + case let .peer(peer): + if !transaction.media.isEmpty { + titleText = strings.Stars_Transaction_MediaPurchase } else { - photoCount += 1 + titleText = transaction.title ?? peer.compactDisplayTitle } - } - if photoCount > 0 && videoCount > 0 { - description += strings.Stars_Transaction_MediaAnd(strings.Stars_Transaction_Photos(photoCount), strings.Stars_Transaction_Videos(videoCount)).string - } else if photoCount > 0 { - if photoCount > 1 { - description += strings.Stars_Transaction_Photos(photoCount) + via = nil + case .appStore: + titleText = strings.Stars_Transaction_AppleTopUp_Title + via = strings.Stars_Transaction_AppleTopUp_Subtitle + case .playMarket: + titleText = strings.Stars_Transaction_GoogleTopUp_Title + via = strings.Stars_Transaction_GoogleTopUp_Subtitle + case .premiumBot: + titleText = strings.Stars_Transaction_PremiumBotTopUp_Title + via = strings.Stars_Transaction_PremiumBotTopUp_Subtitle + case .fragment: + if parentPeer.id == component.context.account.peerId { + titleText = strings.Stars_Transaction_FragmentTopUp_Title + via = strings.Stars_Transaction_FragmentTopUp_Subtitle } else { - description += strings.Stars_Transaction_SinglePhoto + titleText = strings.Stars_Transaction_FragmentWithdrawal_Title + via = strings.Stars_Transaction_FragmentWithdrawal_Subtitle } - } else if videoCount > 0 { - if videoCount > 1 { - description += strings.Stars_Transaction_Videos(videoCount) - } else { - description += strings.Stars_Transaction_SingleVideo + case .ads: + titleText = strings.Stars_Transaction_TelegramAds_Title + via = strings.Stars_Transaction_TelegramAds_Subtitle + case .unsupported: + titleText = strings.Stars_Transaction_Unsupported_Title + via = nil + } + if !transaction.media.isEmpty { + var description: String = "" + var photoCount: Int32 = 0 + var videoCount: Int32 = 0 + for media in transaction.media { + if let _ = media as? TelegramMediaFile { + videoCount += 1 + } else { + photoCount += 1 + } } + if photoCount > 0 && videoCount > 0 { + description += strings.Stars_Transaction_MediaAnd(strings.Stars_Transaction_Photos(photoCount), strings.Stars_Transaction_Videos(videoCount)).string + } else if photoCount > 0 { + if photoCount > 1 { + description += strings.Stars_Transaction_Photos(photoCount) + } else { + description += strings.Stars_Transaction_SinglePhoto + } + } else if videoCount > 0 { + if videoCount > 1 { + description += strings.Stars_Transaction_Videos(videoCount) + } else { + description += strings.Stars_Transaction_SingleVideo + } + } + descriptionText = description + } else { + descriptionText = transaction.description ?? "" } - descriptionText = description - } else { - descriptionText = transaction.description ?? "" - } - - messageId = transaction.paidMessageId - - count = transaction.count - transactionId = transaction.id - date = transaction.date - if case let .peer(peer) = transaction.peer { - toPeer = peer - } else { - toPeer = nil + + messageId = transaction.paidMessageId + + count = transaction.count + transactionId = transaction.id + 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) } - transactionPeer = transaction.peer - media = transaction.media - photo = transaction.photo - isRefund = transaction.flags.contains(.isRefund) case let .receipt(receipt): titleText = receipt.invoiceMedia.title descriptionText = receipt.invoiceMedia.description @@ -284,14 +319,63 @@ private final class StarsTransactionSheetContent: CombinedComponent { media = [] photo = receipt.invoiceMedia.photo isRefund = false + isGift = false + delayedCloseOnOpenPeer = false + case let .gift(message): + let incoming = message.flags.contains(.Incoming) + titleText = incoming ? strings.Stars_Gift_Received_Title : strings.Stars_Gift_Sent_Title + let peerName = state.peerMap[message.id.peerId]?.compactDisplayTitle ?? "" + descriptionText = incoming ? strings.Stars_Gift_Received_Text : strings.Stars_Gift_Sent_Text(peerName).string + if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .giftStars(_, _, countValue, _, _, _) = action.action { + count = countValue + if !incoming { + countIsGeneric = true + } + countOnTop = true + transactionId = nil + } 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 } + if let spaceRegex { + let nsRange = NSRange(descriptionText.startIndex..., in: descriptionText) + let matches = spaceRegex.matches(in: descriptionText, options: [], range: nsRange) + var modifiedString = descriptionText + + for match in matches.reversed() { + let matchRange = Range(match.range, in: descriptionText)! + let matchedSubstring = String(descriptionText[matchRange]) + let replacedSubstring = matchedSubstring.replacingOccurrences(of: " ", with: "\u{00A0}") + modifiedString.replaceSubrange(matchRange, with: replacedSubstring) + } + descriptionText = modifiedString + } let formattedAmount = presentationStringsFormattedNumber(abs(Int32(count)), dateTimeFormat.groupingSeparator) - if count < 0 { + let countColor: UIColor + if countIsGeneric { + amountText = "\(formattedAmount)" + countColor = theme.list.itemPrimaryTextColor + } else if count < 0 { amountText = "- \(formattedAmount)" + countColor = theme.list.itemDestructiveColor } else { amountText = "+ \(formattedAmount)" + countColor = theme.list.itemDisclosureActions.constructive.fillColor } additionalText = strings.Stars_Transaction_Terms buttonText = strings.Common_OK @@ -312,7 +396,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) let imageSubject: StarsImageComponent.Subject - if !media.isEmpty { + if isGift { + imageSubject = .gift(count) + } else if !media.isEmpty { imageSubject = .media(media) } else if let photo { imageSubject = .photo(photo) @@ -331,14 +417,14 @@ private final class StarsTransactionSheetContent: CombinedComponent { diameter: 90.0, backgroundColor: theme.actionSheet.opaqueItemBackgroundColor, action: !media.isEmpty ? { transitionNode, addToTransitionSurface in - component.openMedia(media, transitionNode, addToTransitionSurface) + component.openMedia(media.map { $0.media }, transitionNode, addToTransitionSurface) } : nil ), availableSize: CGSize(width: context.availableSize.width, height: 200.0), transition: .immediate ) - let amountAttributedText = NSMutableAttributedString(string: amountText, font: Font.semibold(17.0), textColor: amountText.hasPrefix("-") ? theme.list.itemDestructiveColor : theme.list.itemDisclosureActions.constructive.fillColor) + let amountAttributedText = NSMutableAttributedString(string: amountText, font: Font.semibold(17.0), textColor: countColor) let amount = amount.update( component: BalancedTextComponent( text: .plain(amountAttributedText), @@ -364,16 +450,39 @@ private final class StarsTransactionSheetContent: CombinedComponent { let tableLinkColor = theme.list.itemAccentColor var tableItems: [TableComponent.Item] = [] - if let toPeer { + if isGift, toPeer == nil { + tableItems.append(.init( + id: "from", + title: strings.Stars_Transaction_From, + component: AnyComponent( + Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + peer: nil + ) + ), + action: { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Transaction_FragmentUnknown_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + ) + ) + )) + } else if let toPeer { tableItems.append(.init( id: "to", - title: count < 0 ? strings.Stars_Transaction_To : strings.Stars_Transaction_From, + title: count < 0 || countIsGeneric ? strings.Stars_Transaction_To : strings.Stars_Transaction_From, component: AnyComponent( Button( content: AnyComponent( PeerCellComponent( context: component.context, - textColor: tableLinkColor, + theme: theme, peer: toPeer ) ), @@ -498,8 +607,11 @@ private final class StarsTransactionSheetContent: CombinedComponent { } }, tapAction: { attributes, _ in - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Transaction_Terms_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + if let controller = controller() as? StarsTransactionScreen, 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_Transaction_Terms_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + component.cancel(true) + } } ), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), @@ -538,23 +650,51 @@ private final class StarsTransactionSheetContent: CombinedComponent { var originY: CGFloat = 0.0 originY += star.size.height - 23.0 + var descriptionSize: CGSize = .zero if !descriptionText.isEmpty { + let openAppExamples = component.openAppExamples + + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { + 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 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) + }) + let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) + } let description = description.update( component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: descriptionText, - font: Font.regular(15.0), - textColor: theme.actionSheet.primaryTextColor, - paragraphAlignment: .center - )), + text: .plain(attributedString), horizontalAlignment: .center, - maximumNumberOfLines: 3 + maximumNumberOfLines: 5, + 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: { _, _ in + openAppExamples() + } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) + descriptionSize = description.size + var descriptionOrigin = originY + if countOnTop { + descriptionOrigin += amount.size.height + 13.0 + } context.add(description - .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + description.size.height / 2.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: descriptionOrigin + description.size.height / 2.0)) ) originY += description.size.height + 10.0 } @@ -593,14 +733,20 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) } + var amountOrigin = originY + if countOnTop { + amountOrigin -= descriptionSize.height + 10.0 + originY += amount.size.height + 26.0 + } else { + originY += amount.size.height + 20.0 + } context.add(amount - .position(CGPoint(x: amountOriginX + amount.size.width / 2.0, y: originY + amount.size.height / 2.0)) + .position(CGPoint(x: amountOriginX + amount.size.width / 2.0, y: amountOrigin + amount.size.height / 2.0)) ) context.add(amountStar - .position(CGPoint(x: amountOriginX + amount.size.width + amountSpacing + amountStar.size.width / 2.0, y: originY + amountStar.size.height / 2.0)) + .position(CGPoint(x: amountOriginX + amount.size.width + amountSpacing + amountStar.size.width / 2.0, y: amountOrigin + amountStar.size.height / 2.0)) ) - originY += amount.size.height + 20.0 context.add(table .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) @@ -637,6 +783,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { 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 init( @@ -646,6 +793,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { 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 ) { self.context = context @@ -654,6 +802,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { self.openPeer = openPeer self.openMessage = openMessage self.openMedia = openMedia + self.openAppExamples = openAppExamples self.copyTransactionId = copyTransactionId } @@ -698,6 +847,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { openPeer: context.component.openPeer, openMessage: context.component.openMessage, openMedia: context.component.openMedia, + openAppExamples: context.component.openAppExamples, copyTransactionId: context.component.copyTransactionId )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), @@ -768,6 +918,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { public enum Subject: Equatable { case transaction(StarsContext.State.Transaction, EnginePeer) case receipt(BotPaymentReceipt) + case gift(EngineMessage) } private let context: AccountContext @@ -786,6 +937,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { var openPeerImpl: ((EnginePeer) -> Void)? var openMessageImpl: ((EngineMessage.Id) -> Void)? var openMediaImpl: (([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? + var openAppExamplesImpl: (() -> Void)? var copyTransactionIdImpl: ((String) -> Void)? super.init( context: context, @@ -802,6 +954,9 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { openMedia: { media, transitionNode, addToTransitionSurface in openMediaImpl?(media, transitionNode, addToTransitionSurface) }, + openAppExamples: { + openAppExamplesImpl?() + }, copyTransactionId: { transactionId in copyTransactionIdImpl?(transactionId) } @@ -843,7 +998,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { return } if let navigationController = self.navigationController as? NavigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil)) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil)) } }) } @@ -889,6 +1044,19 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { })) } + openAppExamplesImpl = { [weak self] in + guard let self else { + return + } + let _ = (context.sharedContext.makeMiniAppListScreenInitialData(context: context) + |> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + navigationController.pushViewController(context.sharedContext.makeMiniAppListScreen(context: context, initialData: initialData)) + }) + } + copyTransactionIdImpl = { [weak self] transactionId in guard let self else { return @@ -1166,12 +1334,12 @@ private final class TableComponent: CombinedComponent { private final class PeerCellComponent: Component { let context: AccountContext - let textColor: UIColor + let theme: PresentationTheme let peer: EnginePeer? - init(context: AccountContext, textColor: UIColor, peer: EnginePeer?) { + init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer?) { self.context = context - self.textColor = textColor + self.theme = theme self.peer = peer } @@ -1179,7 +1347,7 @@ private final class PeerCellComponent: Component { if lhs.context !== rhs.context { return false } - if lhs.textColor !== rhs.textColor { + if lhs.theme !== rhs.theme { return false } if lhs.peer != rhs.peer { @@ -1189,18 +1357,14 @@ private final class PeerCellComponent: Component { } final class View: UIView { - private let avatarNode: AvatarNode + private let avatar = ComponentView() private let text = ComponentView() private var component: PeerCellComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0)) - super.init(frame: frame) - - self.addSubnode(self.avatarNode) } required init?(coder: NSCoder) { @@ -1211,21 +1375,33 @@ private final class PeerCellComponent: Component { self.component = component self.state = state - self.avatarNode.setPeer( - context: component.context, - theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, - peer: component.peer, - synchronousLoad: true - ) - let avatarSize = CGSize(width: 22.0, height: 22.0) let spacing: CGFloat = 6.0 + let peerName: String + let peer: StarsContext.State.Transaction.Peer + if let peerValue = component.peer { + peerName = peerValue.compactDisplayTitle + peer = .peer(peerValue) + } else { + peerName = "Unknown User" + peer = .fragment + } + + let avatarNaturalSize = self.avatar.update( + transition: .immediate, + component: AnyComponent( + StarsAvatarComponent(context: component.context, theme: component.theme, peer: peer, photo: nil, media: [], backgroundColor: .clear) + ), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let textSize = self.text.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: component.peer?.compactDisplayTitle ?? "", font: Font.regular(15.0), textColor: component.textColor, paragraphAlignment: .left)) + text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.theme.list.itemAccentColor, paragraphAlignment: .left)) ) ), environment: {}, @@ -1235,7 +1411,15 @@ private final class PeerCellComponent: Component { let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height) let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize) - self.avatarNode.frame = avatarFrame + + if let view = self.avatar.view { + if view.superview == nil { + self.addSubview(view) + } + let scale = avatarSize.width / avatarNaturalSize.width + view.transform = CGAffineTransform(scaleX: scale, y: scale) + view.frame = avatarFrame + } if let view = self.text.view { if view.superview == nil { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift index 243e28cd2a6..e84f47e28e2 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift @@ -23,6 +23,7 @@ final class StarsBalanceComponent: Component { let actionCooldownUntilTimestamp: Int32? let action: () -> Void let buyAds: (() -> Void)? + let additionalAction: AnyComponent? init( theme: PresentationTheme, @@ -35,7 +36,8 @@ final class StarsBalanceComponent: Component { actionIsEnabled: Bool, actionCooldownUntilTimestamp: Int32? = nil, action: @escaping () -> Void, - buyAds: (() -> Void)? + buyAds: (() -> Void)?, + additionalAction: AnyComponent? = nil ) { self.theme = theme self.strings = strings @@ -48,6 +50,7 @@ final class StarsBalanceComponent: Component { self.actionCooldownUntilTimestamp = actionCooldownUntilTimestamp self.action = action self.buyAds = buyAds + self.additionalAction = additionalAction } static func ==(lhs: StarsBalanceComponent, rhs: StarsBalanceComponent) -> Bool { @@ -88,6 +91,8 @@ final class StarsBalanceComponent: Component { private var button = ComponentView() private var buyAdsButton = ComponentView() + private var additionalButton = ComponentView() + private var component: StarsBalanceComponent? private weak var state: EmptyComponentState? @@ -163,7 +168,7 @@ final class StarsBalanceComponent: Component { let subtitleText: String if let rate = component.rate { - subtitleText = "≈\(formatUsdValue(component.count, rate: rate))" + subtitleText = "≈\(formatTonUsdValue(component.count, divide: false, rate: rate, dateTimeFormat: component.dateTimeFormat))" } else { subtitleText = component.strings.Stars_Intro_YourBalance } @@ -275,9 +280,29 @@ final class StarsBalanceComponent: Component { } } + contentHeight += buttonSize.height + } + + if let additionalAction = component.additionalAction { + contentHeight += 18.0 + let buttonSize = self.additionalButton.update( + transition: transition, + component: additionalAction, + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 50.0) + ) + if let buttonView = self.additionalButton.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: contentHeight), size: buttonSize) + buttonView.frame = buttonFrame + } contentHeight += buttonSize.height + contentHeight += 2.0 } + contentHeight += sideInset return CGSize(width: availableSize.width, height: contentHeight) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift index dfe6aeafabf..69179417434 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift @@ -7,6 +7,7 @@ import AccountContext import MultilineTextComponent import TelegramPresentationData import PresentationDataUtils +import TelegramStringFormatting final class StarsOverviewItemComponent: Component { let theme: PresentationTheme @@ -80,7 +81,7 @@ final class StarsOverviewItemComponent: Component { } let valueString = presentationStringsFormattedNumber(Int32(component.value), component.dateTimeFormat.groupingSeparator) - let usdValueString = formatUsdValue(component.value, rate: component.rate) + let usdValueString = formatTonUsdValue(component.value, divide: false, rate: component.rate, dateTimeFormat: component.dateTimeFormat) let valueSize = self.value.update( transition: .immediate, diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift index de36ea1c50c..ee6ff8b47ff 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift @@ -193,6 +193,10 @@ final class StarsStatisticsScreenComponent: Component { deinit { self.stateDisposable?.dispose() } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.scrollView.contentInset.top), animated: true) + } func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { @@ -462,8 +466,10 @@ final class StarsStatisticsScreenComponent: Component { return nil } }, - tapAction: { attributes, _ in - component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_BotRevenue_Withdraw_Info_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + tapAction: { [weak self] attributes, _ in + if let controller = self?.controller?() as? StarsStatisticsScreen, let navigationController = controller.navigationController as? NavigationController { + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_BotRevenue_Withdraw_Info_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } } )), items: [AnyComponentWithIdentity(id: 0, component: AnyComponent( @@ -783,6 +789,13 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer { } self.transactionsContext.loadMore() + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? StarsStatisticsScreenComponent.View else { + return + } + componentView.scrollToTop() + } } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index 2e8c2b64418..61a909dff37 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -203,12 +203,13 @@ final class StarsTransactionsListPanelComponent: Component { let fontBaseDisplaySize = 17.0 - let itemTitle: String + var itemTitle: String let itemSubtitle: String? var itemDate: String + var itemPeer = item.peer switch item.peer { case let .peer(peer): - if !item.media.isEmpty { + if !item.media.isEmpty { itemTitle = environment.strings.Stars_Intro_Transaction_MediaPurchase itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) } else if let title = item.title { @@ -216,7 +217,11 @@ final class StarsTransactionsListPanelComponent: Component { itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) } else { itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) - itemSubtitle = nil + if item.flags.contains(.isGift) { + itemSubtitle = environment.strings.Stars_Intro_Transaction_Gift_Title + } else { + itemSubtitle = nil + } } case .appStore: itemTitle = environment.strings.Stars_Intro_Transaction_AppleTopUp_Title @@ -226,8 +231,14 @@ final class StarsTransactionsListPanelComponent: Component { itemSubtitle = environment.strings.Stars_Intro_Transaction_GoogleTopUp_Subtitle case .fragment: if component.isAccount { - itemTitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Title - itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Subtitle + if item.flags.contains(.isGift) { + itemTitle = environment.strings.Stars_Intro_Transaction_Gift_UnknownUser + itemSubtitle = environment.strings.Stars_Intro_Transaction_Gift_Title + itemPeer = .fragment + } else { + itemTitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Title + itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Subtitle + } } else { itemTitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Title itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Subtitle @@ -298,7 +309,7 @@ final class StarsTransactionsListPanelComponent: Component { theme: environment.theme, title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), contentInsets: UIEdgeInsets(top: 9.0, left: environment.containerInsets.left, bottom: 8.0, right: environment.containerInsets.right), - leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: item.peer, photo: item.photo, media: item.media, backgroundColor: environment.theme.list.plainBackgroundColor))), false), + leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: itemPeer, photo: item.photo, media: item.media, backgroundColor: environment.theme.list.plainBackgroundColor))), false), icon: nil, accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), action: { [weak self] _ in @@ -379,7 +390,17 @@ final class StarsTransactionsListPanelComponent: Component { let wasEmpty = self.items.isEmpty let hadLocalTransactions = self.items.contains(where: { $0.flags.contains(.isLocal) }) - self.items = status.transactions + var existingIds = Set() + var filteredItems: [StarsContext.State.Transaction] = [] + for transaction in status.transactions { + let id = transaction.extendedId + if !existingIds.contains(id) { + existingIds.insert(id) + filteredItems.append(transaction) + } + } + + self.items = filteredItems if !status.isLoading { self.currentLoadMoreId = nil } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index cd66fc13902..d0ff4ecdb2e 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -26,17 +26,20 @@ final class StarsTransactionsScreenComponent: Component { let starsContext: StarsContext let openTransaction: (StarsContext.State.Transaction) -> Void let buy: () -> Void + let gift: () -> Void init( context: AccountContext, starsContext: StarsContext, openTransaction: @escaping (StarsContext.State.Transaction) -> Void, - buy: @escaping () -> Void + buy: @escaping () -> Void, + gift: @escaping () -> Void ) { self.context = context self.starsContext = starsContext self.openTransaction = openTransaction self.buy = buy + self.gift = gift } static func ==(lhs: StarsTransactionsScreenComponent, rhs: StarsTransactionsScreenComponent) -> Bool { @@ -89,6 +92,8 @@ final class StarsTransactionsScreenComponent: Component { private let balanceView = ComponentView() + private let subscriptionsView = ComponentView() + private let topBalanceTitleView = ComponentView() private let topBalanceValueView = ComponentView() private let topBalanceIconView = ComponentView() @@ -282,6 +287,7 @@ final class StarsTransactionsScreenComponent: Component { } let environment = environment[ViewControllerComponentContainer.Environment.self].value + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } if self.stateDisposable == nil { self.stateDisposable = (component.starsContext.state @@ -531,7 +537,27 @@ final class StarsTransactionsScreenComponent: Component { } component.buy() }, - buyAds: nil + buyAds: nil, + additionalAction: premiumConfiguration.starsGiftsPurchaseAvailable ? AnyComponent( + Button( + content: AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(BundleIconComponent(name: "Premium/Stars/Gift", tintColor: environment.theme.list.itemAccentColor)) + ), + AnyComponentWithIdentity( + id: "label", + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Stars_Intro_GiftStars, font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor)))) + ) + ], + spacing: 6.0) + ), + action: { + component.gift() + } + ) + ) : nil ) ))] )), @@ -545,10 +571,42 @@ final class StarsTransactionsScreenComponent: Component { } starTransition.setFrame(view: balanceView, frame: balanceFrame) } - contentHeight += balanceSize.height contentHeight += 44.0 + let subscriptionsItems: [AnyComponentWithIdentity] = [] + + if !subscriptionsItems.isEmpty { + //TODO:localize + let subscriptionsSize = self.subscriptionsView.update( + transition: .immediate, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "My Subscriptions".uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: subscriptionsItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) + ) + let subscriptionsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - subscriptionsSize.width) / 2.0), y: contentHeight), size: subscriptionsSize) + if let subscriptionsView = self.subscriptionsView.view { + if subscriptionsView.superview == nil { + self.scrollView.addSubview(subscriptionsView) + } + starTransition.setFrame(view: subscriptionsView, frame: subscriptionsFrame) + } + contentHeight += subscriptionsSize.height + contentHeight += 44.0 + } + let initialTransactions = self.starsState?.transactions ?? [] var panelItems: [StarsTransactionsPanelContainerComponent.Item] = [] if !initialTransactions.isEmpty { @@ -704,6 +762,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { self.starsContext = starsContext var buyImpl: (() -> Void)? + var giftImpl: (() -> Void)? var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)? super.init(context: context, component: StarsTransactionsScreenComponent( context: context, @@ -713,6 +772,9 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { }, buy: { buyImpl?() + }, + gift: { + giftImpl?() } ), navigationBarAppearance: .transparent) @@ -744,7 +806,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { guard let self else { return } - let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: nil, requiredStars: nil, completion: { [weak self] stars in + let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic(requiredStars: nil), completion: { [weak self] stars in guard let self else { return } @@ -753,12 +815,14 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let resultController = UndoOverlayController( presentationData: presentationData, - content: .image( - image: UIImage(bundleImageName: "Premium/Stars/StarLarge")!, - title: presentationData.strings.Stars_Intro_PurchasedTitle, + content: .universal( + animation: "StarsBuy", + scale: 0.066, + colors: [:], + title: presentationData.strings.Stars_Intro_PurchasedTitle, text: presentationData.strings.Stars_Intro_PurchasedText(presentationData.strings.Stars_Intro_PurchasedText_Stars(Int32(stars))).string, - round: false, - undoText: nil + customUndoText: nil, + timeout: nil ), elevatedLayout: false, action: { _ in return true}) @@ -768,6 +832,75 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { }) } + giftImpl = { [weak self] in + guard let self else { + return + } + let _ = combineLatest(queue: Queue.mainQueue(), + self.options.get() |> take(1), + self.context.account.stateManager.contactBirthdays |> take(1) + ).start(next: { [weak self] options, birthdays in + guard let self else { + return + } + let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .stars(birthdays), completion: { [weak self] peerIds in + guard let self, let peerId = peerIds.first else { + return + } + let purchaseController = self.context.sharedContext.makeStarsPurchaseScreen( + context: self.context, + starsContext: starsContext, + options: options, + purpose: .gift(peerId: peerId), + completion: { [weak self] stars in + guard let self else { + return + } + + if let navigationController = self.navigationController as? NavigationController { + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is ContactSelectionController) } + navigationController.setViewControllers(controllers, animated: true) + } + + Queue.mainQueue().after(2.0) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .universal( + animation: "StarsSend", + scale: 0.066, + colors: [:], + title: nil, + text: presentationData.strings.Stars_Intro_StarsSent(Int32(stars)), + customUndoText: presentationData.strings.Stars_Intro_StarsSent_ViewChat, + timeout: nil + ), + elevatedLayout: false, + action: { [weak self] action in + if case .undo = action, let navigationController = self?.navigationController as? NavigationController { + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: true, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true)) + }) + } + return true + }) + self.present(resultController, in: .window(.root)) + } + } + ) + self.push(purchaseController) + }) + self.push(controller) + }) + } + self.starsContext.load(force: false) } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsUtils.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsUtils.swift deleted file mode 100644 index f09a7ed3ce6..00000000000 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsUtils.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -func formatUsdValue(_ value: Int64, rate: Double) -> String { - let formattedValue = String(format: "%0.2f", (Double(value)) * rate) - return "$\(formattedValue)" -} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD index d19426478da..35f40fa69f8 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD @@ -31,7 +31,8 @@ swift_library( "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", - "//submodules/TelegramUI/Components/Stars/StarsImageComponent", + "//submodules/TelegramUI/Components/Stars/StarsImageComponent", + "//submodules/ConfettiEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 632e45c61f7..913a0eff7ea 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -18,6 +18,7 @@ import UndoUI import AccountContext import PresentationDataUtils import StarsImageComponent +import ConfettiEffect private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -256,9 +257,7 @@ private final class SheetContent: CombinedComponent { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let theme = presentationData.theme let strings = presentationData.strings - -// let sideInset: CGFloat = 16.0 + environment.safeInsets.left - + var contentSize = CGSize(width: context.availableSize.width, height: 18.0) let background = background.update( @@ -443,11 +442,11 @@ private final class SheetContent: CombinedComponent { ) if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme { - state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, theme) + state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme) } let amountString = presentationStringsFormattedNumber(Int32(amount), presentationData.dateTimeFormat.groupingSeparator) - let buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amountString)", font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) + let 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)) @@ -479,12 +478,19 @@ private final class SheetContent: CombinedComponent { state?.buy(requestTopUp: { [weak controller] completion in let premiumConfiguration = PremiumConfiguration.with(appConfiguration: accountContext.currentAppConfiguration.with { $0 }) if !premiumConfiguration.isPremiumDisabled { + let purpose: StarsPurchasePurpose + if isMedia { + purpose = .unlockMedia(requiredStars: invoice.totalAmount) + } else if let peerId = state?.botPeer?.id { + purpose = .transfer(peerId: peerId, requiredStars: invoice.totalAmount) + } else { + purpose = .generic(requiredStars: nil) + } let purchaseController = accountContext.sharedContext.makeStarsPurchaseScreen( context: accountContext, starsContext: starsContext, options: state?.options ?? [], - peerId: isMedia ? nil : state?.botPeer?.id, - requiredStars: invoice.totalAmount, + purpose: purpose, completion: { [weak starsContext] stars in starsContext?.add(balance: stars) Queue.mainQueue().after(0.1) { @@ -512,12 +518,21 @@ 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")!, +// 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, text: text, - round: false, - undoText: nil + customUndoText: nil, + timeout: nil ), elevatedLayout: lastController is ChatController, action: { _ in return true} @@ -658,6 +673,7 @@ private final class StarsTransferSheetComponent: CombinedComponent { public final class StarsTransferScreen: ViewControllerComponentContainer { private let context: AccountContext + private let extendedMedia: [TelegramExtendedMedia] private let completion: (Bool) -> Void public init( @@ -670,6 +686,7 @@ public final class StarsTransferScreen: ViewControllerComponentContainer { completion: @escaping (Bool) -> Void ) { self.context = context + self.extendedMedia = extendedMedia self.completion = completion super.init( @@ -707,6 +724,10 @@ public final class StarsTransferScreen: ViewControllerComponentContainer { } self.didComplete = true self.completion(paid) + + if !self.extendedMedia.isEmpty && paid { + self.navigationController?.view.addSubview(ConfettiView(frame: self.view.bounds, customImage: UIImage(bundleImageName: "Peer Info/PremiumIcon"))) + } } public func dismissAnimated() { diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 436374d47e0..0270432457a 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -66,6 +66,8 @@ private final class SheetContent: CombinedComponent { let component = context.component let state = context.state + let controller = environment.controller + let theme = environment.theme.withModalBlocksBackground() let strings = environment.strings let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } @@ -229,7 +231,9 @@ private final class SheetContent: CombinedComponent { } }, tapAction: { attributes, _ in - component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_PaidContent_AmountInfo_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + if let controller = controller() as? StarsWithdrawScreen, let navigationController = controller.navigationController as? NavigationController { + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_PaidContent_AmountInfo_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } } )) case let .reaction(starsToTop): @@ -297,17 +301,16 @@ private final class SheetContent: CombinedComponent { } if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme { - state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, theme) + state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme) } - let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) + let buttonAttributedString = NSMutableAttributedString(string: buttonString, 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)) buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string)) } - let controller = environment.controller let button = button.update( component: ButtonComponent( background: ButtonComponent.Background( diff --git a/submodules/TelegramUI/Components/StickerPickerScreen/BUILD b/submodules/TelegramUI/Components/StickerPickerScreen/BUILD index c6fbd0fa829..cd36861d517 100644 --- a/submodules/TelegramUI/Components/StickerPickerScreen/BUILD +++ b/submodules/TelegramUI/Components/StickerPickerScreen/BUILD @@ -33,12 +33,15 @@ swift_library( "//submodules/TelegramUI/Components/EntityKeyboard", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/LottieComponentResourceContent", "//submodules/ChatPresentationInterfaceState", "//submodules/TelegramUI/Components/MediaEditor", "//submodules/TelegramUI/Components/CameraButtonComponent", "//submodules/ContextUI", "//submodules/UndoUI", "//submodules/GalleryUI", + "//submodules/TelegramUI/Components/TextLoadingEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift index 80ee4b55596..99046442ec2 100644 --- a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift +++ b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift @@ -20,8 +20,12 @@ import MediaEditor import EntityKeyboardGifContent import CameraButtonComponent import BundleIconComponent +import LottieComponent +import LottieComponentResourceContent import UndoUI import GalleryUI +import TextLoadingEffect +import TelegramStringFormatting private final class StickerSelectionComponent: Component { typealias EnvironmentType = Empty @@ -532,7 +536,10 @@ public class StickerPickerScreen: ViewController { self.containerView.addSubview(self.hostView) if controller.hasInteractiveStickers { - self.storyStickersContentView = StoryStickersContentView(isPremium: context.isPremium) + self.storyStickersContentView = StoryStickersContentView( + context: context, + weather: controller.weather + ) self.storyStickersContentView?.locationAction = { [weak self] in self?.controller?.presentLocationPicker() } @@ -552,6 +559,9 @@ public class StickerPickerScreen: ViewController { self.presentLinkPremiumSuggestion() } } + self.storyStickersContentView?.weatherAction = { [weak self] in + self?.controller?.addWeather() + } } let gifItems: Signal @@ -2041,15 +2051,37 @@ public class StickerPickerScreen: ViewController { return self.displayNode as! Node } + public enum Weather { + public struct LoadedWeather { + public let emoji: String + public let emojiFile: TelegramMediaFile + public let temperature: Double + + public init(emoji: String, emojiFile: TelegramMediaFile, temperature: Double) { + self.emoji = emoji + self.emojiFile = emojiFile + self.temperature = temperature + } + } + + case none + case notDetermined + case notAllowed + case notPreloaded + case fetching + case loaded(StickerPickerScreen.Weather.LoadedWeather) + } + private let context: AccountContext private let theme: PresentationTheme - fileprivate let forceDark: Bool + let forceDark: Bool private let inputData: Signal - fileprivate let defaultToEmoji: Bool + let defaultToEmoji: Bool let isFullscreen: Bool let hasEmoji: Bool let hasGifs: Bool let hasInteractiveStickers: Bool + let weather: Signal private var currentLayout: ContainerViewLayout? @@ -2063,8 +2095,9 @@ public class StickerPickerScreen: ViewController { public var presentAudioPicker: () -> Void = { } public var addReaction: () -> Void = { } public var addLink: () -> Void = { } + public var addWeather: () -> Void = { } - public init(context: AccountContext, inputData: Signal, forceDark: Bool = false, expanded: Bool = false, defaultToEmoji: Bool = false, hasEmoji: Bool = true, hasGifs: Bool = false, hasInteractiveStickers: Bool = true) { + public init(context: AccountContext, inputData: Signal, forceDark: Bool = false, expanded: Bool = false, defaultToEmoji: Bool = false, hasEmoji: Bool = true, hasGifs: Bool = false, hasInteractiveStickers: Bool = true, weather: Signal = .single(.none)) { self.context = context let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.theme = forceDark ? defaultDarkColorPresentationTheme : presentationData.theme @@ -2075,6 +2108,7 @@ public class StickerPickerScreen: ViewController { self.hasEmoji = hasEmoji self.hasGifs = hasGifs self.hasInteractiveStickers = hasInteractiveStickers + self.weather = weather super.init(navigationBarPresentationData: expanded ? NavigationBarPresentationData(presentationData: presentationData) : nil) @@ -2137,22 +2171,28 @@ public class StickerPickerScreen: ViewController { } private final class InteractiveStickerButtonContent: Component { + let context: AccountContext let theme: PresentationTheme - let title: String - let iconName: String + let title: String? + let iconName: String? + let iconFile: TelegramMediaFile? let useOpaqueTheme: Bool weak var tintContainerView: UIView? public init( + context: AccountContext, theme: PresentationTheme, - title: String, - iconName: String, + title: String?, + iconName: String?, + iconFile: TelegramMediaFile? = nil, useOpaqueTheme: Bool, tintContainerView: UIView ) { + self.context = context self.theme = theme self.title = title self.iconName = iconName + self.iconFile = iconFile self.useOpaqueTheme = useOpaqueTheme self.tintContainerView = tintContainerView } @@ -2181,6 +2221,7 @@ private final class InteractiveStickerButtonContent: Component { private let backgroundLayer = SimpleLayer() let tintBackgroundLayer = SimpleLayer() + private var loadingView: TextLoadingEffectView? private var icon: ComponentView private var title: ComponentView @@ -2203,57 +2244,105 @@ private final class InteractiveStickerButtonContent: Component { func update(component: InteractiveStickerButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.backgroundLayer.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11).cgColor - - let iconSize = self.icon.update( - transition: .immediate, - component: AnyComponent(BundleIconComponent( - name: component.iconName, - tintColor: .white, - maxSize: CGSize(width: 20.0, height: 20.0) - )), - environment: {}, - containerSize: availableSize - ) - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(Text( - text: component.title.uppercased(), - font: Font.with(size: 23.0, design: .camera), - color: .white - )), - environment: {}, - containerSize: availableSize - ) - - let padding: CGFloat = 7.0 - let spacing: CGFloat = 4.0 - let buttonSize = CGSize(width: padding + iconSize.width + spacing + titleSize.width + padding, height: 34.0) - - if let view = self.icon.view { - if view.superview == nil { - self.addSubview(view) + + let iconSize: CGSize + let buttonSize: CGSize + if let title = component.title { + if let iconFile = component.iconFile { + iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent( + LottieComponent( + content: LottieComponent.ResourceContent(context: component.context, file: iconFile, attemptSynchronously: true, providesPlaceholder: true), + color: nil, + placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.4), + loop: !["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"].contains(component.iconName ?? "") + ) + ), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + } else { + iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: component.iconName ?? "", + tintColor: .white, + maxSize: CGSize(width: 20.0, height: 20.0) + )), + environment: {}, + containerSize: availableSize + ) } - transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((buttonSize.height - iconSize.height) / 2.0)), size: iconSize)) - } - if let view = self.title.view { - if view.superview == nil { - self.addSubview(view) + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(Text( + text: title.uppercased(), + font: Font.with(size: 23.0, design: .camera), + color: .white + )), + environment: {}, + containerSize: availableSize + ) + + let padding: CGFloat = 7.0 + let spacing: CGFloat = 4.0 + buttonSize = CGSize(width: padding + iconSize.width + spacing + titleSize.width + padding, height: 34.0) + + if let view = self.icon.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((buttonSize.height - iconSize.height) / 2.0)), size: iconSize)) + } + if let view = self.title.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: padding + iconSize.width + spacing, y: floorToScreenPixels((buttonSize.height - titleSize.height) / 2.0)), size: titleSize)) + } + + if let loadingView = self.loadingView { + self.loadingView = nil + loadingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + loadingView.removeFromSuperview() + }) + } + } else { + buttonSize = CGSize(width: 87.0, height: 34.0) + + let loadingView: TextLoadingEffectView + if let current = self.loadingView { + loadingView = current + } else { + loadingView = TextLoadingEffectView() + self.addSubview(loadingView) + self.loadingView = loadingView } - transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: padding + iconSize.width + spacing, y: floorToScreenPixels((buttonSize.height - titleSize.height) / 2.0)), size: titleSize)) } self.backgroundLayer.cornerRadius = 6.0 self.tintBackgroundLayer.cornerRadius = 6.0 - self.backgroundLayer.frame = CGRect(origin: .zero, size: buttonSize) + var transition = transition + if self.backgroundLayer.frame.width.isZero { + transition = .immediate + } + transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: buttonSize)) if self.tintBackgroundLayer.superlayer == nil, let tintContainerView = component.tintContainerView { Queue.mainQueue().justDispatch { let mappedFrame = self.convert(self.bounds, to: tintContainerView) - self.tintBackgroundLayer.frame = mappedFrame + transition.setFrame(layer: self.tintBackgroundLayer, frame: mappedFrame) } } + if let loadingView = self.loadingView { + let loadingSize = CGSize(width: buttonSize.width - 18.0, height: 16.0) + loadingView.update(color: UIColor.white, rect: CGRect(origin: .zero, size: loadingSize)) + loadingView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((buttonSize.width - loadingSize.width) / 2.0), y: floorToScreenPixels((buttonSize.width - loadingSize.width) / 2.0)), size: loadingSize) + } + return buttonSize } } @@ -2423,12 +2512,14 @@ final class ItemStack: CombinedComponent { private let padding: CGFloat private let minSpacing: CGFloat private let verticalSpacing: CGFloat + private let maxHorizontalItems: Int - init(_ items: [AnyComponentWithIdentity], padding: CGFloat, minSpacing: CGFloat, verticalSpacing: CGFloat) { + init(_ items: [AnyComponentWithIdentity], padding: CGFloat, minSpacing: CGFloat, verticalSpacing: CGFloat, maxHorizontalItems: Int) { self.items = items self.padding = padding self.minSpacing = minSpacing self.verticalSpacing = verticalSpacing + self.maxHorizontalItems = maxHorizontalItems } static func ==(lhs: ItemStack, rhs: ItemStack) -> Bool { @@ -2444,6 +2535,9 @@ final class ItemStack: CombinedComponent { if lhs.verticalSpacing != rhs.verticalSpacing { return false } + if lhs.maxHorizontalItems != rhs.maxHorizontalItems { + return false + } return true } @@ -2473,7 +2567,7 @@ final class ItemStack: CombinedComponent { let remainingWidth = context.availableSize.width - itemsWidth - context.component.padding * 2.0 let spacing = remainingWidth / CGFloat(rowItemsCount - 1) - if spacing < context.component.minSpacing || currentGroup.count == 2 { + if spacing < context.component.minSpacing || currentGroup.count == context.component.maxHorizontalItems { groups.append(currentGroup) currentGroup = [] } @@ -2527,124 +2621,226 @@ final class ItemStack: CombinedComponent { } final class StoryStickersContentView: UIView, EmojiCustomContentView { + private let context: AccountContext + let tintContainerView = UIView() - private let container = ComponentView() - private let isPremium: Bool + private var weatherDisposable: Disposable? + private var weather: StickerPickerScreen.Weather = .none var locationAction: () -> Void = {} var audioAction: () -> Void = {} var reactionAction: () -> Void = {} var linkAction: () -> Void = {} + var weatherAction: () -> Void = {} + + private var params: (theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize)? - init(isPremium: Bool) { - self.isPremium = isPremium + init(context: AccountContext, weather: Signal) { + self.context = context super.init(frame: .zero) + + self.weatherDisposable = (weather + |> deliverOnMainQueue).start(next: { [weak self] weather in + guard let self else { + return + } + self.weather = weather + if let (theme, strings, useOpaqueTheme, availableSize) = self.params { + let _ = self.update(theme: theme, strings: strings, useOpaqueTheme: useOpaqueTheme, availableSize: availableSize, transition: .easeInOut(duration: 0.25)) + } + }) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + self.weatherDisposable?.dispose() + } + func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + self.params = (theme, strings, useOpaqueTheme, availableSize) + let padding: CGFloat = 22.0 - let size = self.container.update( - transition: transition, - component: AnyComponent( - ItemStack( - [ - AnyComponentWithIdentity( - id: "link", + var maxHorizontalItems = 2 + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "link", + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( + id: "content", component: AnyComponent( - CameraButton( - content: AnyComponentWithIdentity( - id: "content", - component: AnyComponent( - InteractiveStickerButtonContent( - theme: theme, - title: strings.MediaEditor_AddLink, - iconName: self.isPremium ? "Media Editor/Link" : "Media Editor/LinkLocked", - useOpaqueTheme: useOpaqueTheme, - tintContainerView: self.tintContainerView - ) - ) - ), - action: { [weak self] in - if let self { - self.linkAction() - } - }) + InteractiveStickerButtonContent( + context: self.context, + theme: theme, + title: strings.MediaEditor_AddLink, + iconName: self.context.isPremium ? "Media Editor/Link" : "Media Editor/LinkLocked", + useOpaqueTheme: useOpaqueTheme, + tintContainerView: self.tintContainerView + ) ) ), - AnyComponentWithIdentity( - id: "location", + action: { [weak self] in + if let self { + self.linkAction() + } + }) + ) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "location", + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( + id: "content", component: AnyComponent( - CameraButton( - content: AnyComponentWithIdentity( - id: "content", - component: AnyComponent( - InteractiveStickerButtonContent( - theme: theme, - title: strings.MediaEditor_AddLocationShort, - iconName: "Chat/Attach Menu/Location", - useOpaqueTheme: useOpaqueTheme, - tintContainerView: self.tintContainerView - ) - ) - ), - action: { [weak self] in - if let self { - self.locationAction() - } - }) + InteractiveStickerButtonContent( + context: self.context, + theme: theme, + title: strings.MediaEditor_AddLocationShort, + iconName: "Chat/Attach Menu/Location", + useOpaqueTheme: useOpaqueTheme, + tintContainerView: self.tintContainerView + ) ) ), - AnyComponentWithIdentity( + action: { [weak self] in + if let self { + self.locationAction() + } + }) + ) + ) + ) + + if case .none = self.weather { + + } else { + maxHorizontalItems = 3 + + let weatherButtonContent: AnyComponent + switch self.weather { + case .notAllowed, .notDetermined, .notPreloaded: + weatherButtonContent = AnyComponent( + InteractiveStickerButtonContent( + context: self.context, + theme: theme, + title: stringForTemperature(24), + iconName: "☀️", + iconFile: self.context.animatedEmojiStickersValue["☀️"]?.first?.file, + useOpaqueTheme: useOpaqueTheme, + tintContainerView: self.tintContainerView + ) + ) + case let .loaded(weather): + weatherButtonContent = AnyComponent( + InteractiveStickerButtonContent( + context: self.context, + theme: theme, + title: stringForTemperature(weather.temperature), + iconName: weather.emoji, + iconFile: weather.emojiFile, + useOpaqueTheme: useOpaqueTheme, + tintContainerView: self.tintContainerView + ) + ) + case .fetching: + weatherButtonContent = AnyComponent( + InteractiveStickerButtonContent( + context: self.context, + theme: theme, + title: nil, + iconName: nil, + useOpaqueTheme: useOpaqueTheme, + tintContainerView: self.tintContainerView + ) + ) + default: + fatalError() + } + items.append( + AnyComponentWithIdentity( + id: "weather", + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( + id: "weather", + component: weatherButtonContent + ), + action: { [weak self] in + if let self { + self.weatherAction() + } + }) + ) + ) + ) + } + + items.append( + AnyComponentWithIdentity( + id: "audio", + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( id: "audio", component: AnyComponent( - CameraButton( - content: AnyComponentWithIdentity( - id: "audio", - component: AnyComponent( - InteractiveStickerButtonContent( - theme: theme, - title: strings.MediaEditor_AddAudio, - iconName: "Media Editor/Audio", - useOpaqueTheme: useOpaqueTheme, - tintContainerView: self.tintContainerView - ) - ) - ), - action: { [weak self] in - if let self { - self.audioAction() - } - }) + InteractiveStickerButtonContent( + context: self.context, + theme: theme, + title: strings.MediaEditor_AddAudio, + iconName: "Media Editor/Audio", + useOpaqueTheme: useOpaqueTheme, + tintContainerView: self.tintContainerView + ) ) ), - AnyComponentWithIdentity( + action: { [weak self] in + if let self { + self.audioAction() + } + }) + ) + ) + ) + + items.append( + AnyComponentWithIdentity( + id: "reaction", + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( id: "reaction", component: AnyComponent( - CameraButton( - content: AnyComponentWithIdentity( - id: "reaction", - component: AnyComponent( - InteractiveReactionButtonContent(theme: theme) - ) - ), - action: { [weak self] in - if let self { - self.reactionAction() - } - }) + InteractiveReactionButtonContent(theme: theme) ) - ) - ], + ), + action: { [weak self] in + if let self { + self.reactionAction() + } + }) + ) + ) + ) + + let size = self.container.update( + transition: transition, + component: AnyComponent( + ItemStack( + items, padding: 18.0, minSpacing: 8.0, - verticalSpacing: 12.0 + verticalSpacing: 12.0, + maxHorizontalItems: maxHorizontalItems ) ), environment: {}, diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 1e4cebaf79c..65dfe5baa3d 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -2572,7 +2572,7 @@ final class StorageUsageScreenComponent: Component { navigationController: navigationController, context: component.context, chatLocation: chatLocation, - subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), + subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always )) }) @@ -2675,7 +2675,7 @@ final class StorageUsageScreenComponent: Component { navigationController: navigationController, context: component.context, chatLocation: chatLocation, - subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), + subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always )) }) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 4251599d658..e067307edba 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -98,6 +98,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen", "//submodules/TelegramUI/Components/SliderContextItem", "//submodules/TelegramUI/Components/InteractiveTextComponent", + "//submodules/TelegramUI/Components/SaveProgressScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift index 2c660af7495..242839a492f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift @@ -163,6 +163,7 @@ public extension StoryContainerScreen { static func openPeerStoriesCustom( context: AccountContext, peerId: EnginePeer.Id, + focusOnId: Int32? = nil, isHidden: Bool, initialOrder: [EnginePeer.Id] = [], singlePeer: Bool, @@ -173,7 +174,7 @@ public extension StoryContainerScreen { setProgress: @escaping (Signal) -> Void, completion: @escaping (StoryContainerScreen) -> Void = { _ in } ) { - let storyContent = StoryContentContextImpl(context: context, isHidden: isHidden, focusedPeerId: peerId, singlePeer: singlePeer, fixedOrder: initialOrder) + let storyContent = StoryContentContextImpl(context: context, isHidden: isHidden, focusedPeerId: peerId, focusedStoryId: focusOnId, singlePeer: singlePeer, fixedOrder: initialOrder) let signal = storyContent.state |> take(1) |> mapToSignal { state -> Signal in diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift index 488a9953f83..f43af6dd16b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift @@ -128,18 +128,20 @@ final class StoryAuthorInfoComponent: Component { subtitleTruncationType = .middle } else if let author = component.author { let authorName = author.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - let timeString = stringForStoryActivityTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, preciseTime: true, relativeTimestamp: component.timestamp, relativeTo: timestamp, short: true) let combinedString = NSMutableAttributedString() combinedString.append(NSAttributedString(string: authorName, font: Font.medium(11.0), textColor: titleColor)) - if timeString.count < 6 { - combinedString.append(NSAttributedString(string: " • \(timeString)", font: Font.regular(11.0), textColor: subtitleColor)) + if component.timestamp != 0 { + let timeString = stringForStoryActivityTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, preciseTime: true, relativeTimestamp: component.timestamp, relativeTo: timestamp, short: true) + if timeString.count < 6 { + combinedString.append(NSAttributedString(string: " • \(timeString)", font: Font.regular(11.0), textColor: subtitleColor)) + } } if component.isEdited { combinedString.append(NSAttributedString(string: " • \(component.strings.Story_HeaderEdited)", font: Font.regular(11.0), textColor: subtitleColor)) } subtitle = combinedString subtitleTruncationType = .middle - } else { + } else if component.timestamp != 0 { var subtitleString = stringForStoryActivityTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, preciseTime: true, relativeTimestamp: component.timestamp, relativeTo: timestamp) if component.isEdited { subtitleString.append(" • ") @@ -147,6 +149,13 @@ final class StoryAuthorInfoComponent: Component { } subtitle = NSAttributedString(string: subtitleString, font: Font.regular(11.0), textColor: subtitleColor) subtitleTruncationType = .end + } else { + var subtitleString = "" + if component.isEdited { + subtitleString.append(component.strings.Story_HeaderEdited) + } + subtitle = NSAttributedString(string: subtitleString, font: Font.regular(11.0), textColor: subtitleColor) + subtitleTruncationType = .end } let titleSize = self.title.update( @@ -170,7 +179,10 @@ final class StoryAuthorInfoComponent: Component { containerSize: CGSize(width: availableSize.width - leftInset, height: availableSize.height) ) - let contentHeight: CGFloat = titleSize.height + spacing + subtitleSize.height + var contentHeight: CGFloat = titleSize.height + if subtitle.length != 0 { + contentHeight += spacing + subtitleSize.height + } let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 2.0 + floor((availableSize.height - contentHeight) * 0.5)), size: titleSize) var subtitleOffset: CGFloat = 0.0 diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index e3e4aaaeacb..a0f38bbe683 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -48,6 +48,7 @@ public final class StoryContentContextImpl: StoryContentContext { self.peerId = peerId self.currentFocusedId = initialFocusedId + self.storedFocusedId = self.currentFocusedId self.currentFocusedIdUpdatedPromise.set(.single(Void())) context.engine.account.viewTracker.refreshCanSendMessagesForPeerIds(peerIds: [peerId]) @@ -600,13 +601,14 @@ public final class StoryContentContextImpl: StoryContentContext { context: AccountContext, isHidden: Bool, focusedPeerId: EnginePeer.Id?, + focusedStoryId: Int32? = nil, singlePeer: Bool, fixedOrder: [EnginePeer.Id] = [] ) { self.context = context self.isHidden = isHidden if let focusedPeerId { - self.focusedItem = (focusedPeerId, nil) + self.focusedItem = (focusedPeerId, focusedStoryId) } self.fixedSubscriptionOrder = fixedOrder @@ -858,9 +860,11 @@ public final class StoryContentContextImpl: StoryContentContext { } var centralIndex: Int? - if let (focusedPeerId, _) = self.focusedItem { + var centralStoryId: Int32? + if let (focusedPeerId, focusedStoryId) = self.focusedItem { if let index = subscriptionItems.firstIndex(where: { $0.peer.id == focusedPeerId }) { centralIndex = index + centralStoryId = focusedStoryId } } if centralIndex == nil { @@ -874,7 +878,7 @@ public final class StoryContentContextImpl: StoryContentContext { if let currentState = self.currentState, let existingContext = currentState.findPeerContext(id: subscriptionItems[centralIndex].peer.id) { centralPeerContext = existingContext } else { - centralPeerContext = PeerContext(context: self.context, peerId: subscriptionItems[centralIndex].peer.id, focusedId: nil, loadIds: loadIds) + centralPeerContext = PeerContext(context: self.context, peerId: subscriptionItems[centralIndex].peer.id, focusedId: centralStoryId, loadIds: loadIds) } var previousPeerContext: PeerContext? @@ -1816,7 +1820,7 @@ public func preloadStoryMedia(context: AccountContext, info: StoryPreloadInfo) - case let .file(file): var fetchRange: (Range, MediaBoxFetchPriority)? for attribute in file.attributes { - if case let .Video(_, _, _, preloadSize) = attribute { + if case let .Video(_, _, _, preloadSize, _) = attribute { if let preloadSize { fetchRange = (0 ..< Int64(preloadSize), .default) } @@ -2041,7 +2045,7 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine case let .file(file): var fetchRange: (Range, MediaBoxFetchPriority)? for attribute in file.attributes { - if case let .Video(_, _, _, preloadSize) = attribute { + if case let .Video(_, _, _, preloadSize, _) = attribute { if let preloadSize { fetchRange = (0 ..< Int64(preloadSize), .default) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 6416406beb5..6e447801155 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -1136,7 +1136,7 @@ private final class StoryContainerScreenComponent: Component { var isSilentVideo = false if case let .file(file) = slice.item.storyItem.media { for attribute in file.attributes { - if case let .Video(_, _, flags, _) = attribute { + if case let .Video(_, _, flags, _, _) = attribute { if flags.contains(.isSilent) { isSilentVideo = true } @@ -1654,7 +1654,12 @@ private final class StoryContainerScreenComponent: Component { environment.controller()?.dismiss() } - let _ = component.context.engine.messages.deleteStories(peerId: slice.peer.id, ids: [slice.item.storyItem.id]).start() + if case let .user(user) = slice.peer, user.botInfo != nil { + //TODO:localize + 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() + } } }, markAsSeen: { [weak self] id in @@ -1663,6 +1668,18 @@ private final class StoryContainerScreenComponent: Component { } component.content.markAsSeen(id: id) }, + reorder: { [weak self] in + guard let self, let environment = self.environment else { + return + } + var performReorderAction: (() -> Void)? + if let controller = environment.controller() as? StoryContainerScreen { + performReorderAction = controller.performReorderAction + } + environment.controller()?.dismiss(completion: { + performReorderAction?() + }) + }, controller: { [weak self] in return self?.environment?.controller() }, @@ -2021,6 +2038,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer { } public var customBackAction: (() -> Void)? + public var performReorderAction: (() -> Void)? public init( context: AccountContext, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift index 7f7863caf77..283d05ac45a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift @@ -20,6 +20,7 @@ import LottieComponent import LottieComponentResourceContent import StickerResources import AnimationCache +import TelegramStringFormatting private let shadowImage: UIImage = { return UIImage(bundleImageName: "Stories/ReactionShadow")! @@ -224,12 +225,16 @@ public func storyPreviewWithAddedReactions( } } +private protocol ItemView: UIView { + +} + final class StoryItemOverlaysView: UIView { static let counterFont: UIFont = { return Font.with(size: 17.0, design: .camera, weight: .semibold, traits: .monospacedNumbers) }() - private final class ItemView: HighlightTrackingButton { + private final class ReactionView: HighlightTrackingButton, ItemView { private let shadowView: UIImageView private let coverView: UIImageView @@ -524,6 +529,137 @@ final class StoryItemOverlaysView: UIView { } } + private final class WeatherView: UIView, ItemView { + private let backgroundView = UIView() + private let directStickerView = ComponentView() + private let text = ComponentView() + + private var file: TelegramMediaFile? + private var textFont: UIFont? + + private var customEmojiLoadDisposable: Disposable? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundView.clipsToBounds = true + if #available(iOS 13.0, *) { + self.backgroundView.layer.cornerCurve = .continuous + } + self.addSubview(self.backgroundView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.customEmojiLoadDisposable?.dispose() + } + + func update( + context: AccountContext, + emoji: String, + emojiFile: TelegramMediaFile?, + temperature: Double, + color: Int32, + synchronous: Bool, + size: CGSize, + cornerRadius: CGFloat, + isActive: Bool + ) -> CGSize { + let itemSize = CGSize(width: floor(size.height * 0.71), height: floor(size.height * 0.71)) + + let backgroundColor = UIColor(argb: UInt32(bitPattern: color)) + let textColor: UIColor + if backgroundColor.lightness > 0.705 { + textColor = .black + } else { + textColor = .white + } + let placeholderColor = textColor.withAlphaComponent(0.1) + + if self.file?.fileId != emojiFile?.fileId, let file = emojiFile { + self.file = file + + self.customEmojiLoadDisposable?.dispose() + self.customEmojiLoadDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: file.resource)).start() + + let _ = self.directStickerView.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.ResourceContent(context: context, file: file, attemptSynchronously: synchronous, providesPlaceholder: true), + placeholderColor: placeholderColor, + renderingScale: 2.0, + loop: true + )), + environment: {}, + containerSize: itemSize + ) + } + + let textFont: UIFont + if let current = self.textFont { + textFont = current + } else { + textFont = Font.with(size: floorToScreenPixels(size.height * 0.69), design: .camera, weight: .semibold, traits: .monospacedNumbers) + self.textFont = textFont + } + + let string = NSMutableAttributedString( + string: stringForTemperature(temperature), + font: textFont, + textColor: textColor + ) + string.addAttribute(.kern, value: -(size.height / 38.0) as NSNumber, range: NSMakeRange(0, string.length)) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent(text: .plain(string)) + ), + environment: {}, + containerSize: CGSize(width: .greatestFiniteMagnitude, height: size.height) + ) + + let leftInset = size.height * 0.058 + let rightInset = size.height * 0.2 + let spacing = size.height * 0.205 + let contentWidth: CGFloat = leftInset + itemSize.width + spacing + textSize.width + rightInset + + if let view = self.text.view { + if view.superview == nil { + self.addSubview(view) + } + let textFrame = CGRect(origin: CGPoint(x: contentWidth - textSize.width - rightInset, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) + let textTransition = ComponentTransition.immediate + textTransition.setFrame(view: view, frame: textFrame) + } + + if let directStickerView = self.directStickerView.view as? LottieComponent.View { + if directStickerView.superview == nil { + self.addSubview(directStickerView) + } + + let stickerFrame = itemSize.centered(around: CGPoint(x: size.height * 0.5 + leftInset, y: size.height * 0.5)) + + let stickerTransition = ComponentTransition.immediate + stickerTransition.setPosition(view: directStickerView, position: stickerFrame.center) + stickerTransition.setBounds(view: directStickerView, bounds: CGRect(origin: CGPoint(), size: stickerFrame.size)) + + directStickerView.externalShouldPlay = isActive + } + + let contentSize = CGSize(width: contentWidth, height: size.height) + + self.backgroundView.backgroundColor = backgroundColor + self.backgroundView.frame = CGRect(origin: .zero, size: contentSize) + self.backgroundView.layer.cornerRadius = cornerRadius + + return contentSize + } + } + private var itemViews: [Int: ItemView] = [:] var activate: ((UIView, MessageReaction.Reaction) -> Void)? var requestUpdate: (() -> Void)? @@ -561,25 +697,37 @@ final class StoryItemOverlaysView: UIView { isActive: Bool, transition: ComponentTransition ) { + func getFrameAndRotation(coordinates: MediaArea.Coordinates, scale: CGFloat = 1.0) -> (frame: CGRect, rotation: CGFloat, cornerRadius: CGFloat)? { + let referenceSize = size + var areaSize = CGSize(width: coordinates.width / 100.0 * referenceSize.width, height: coordinates.height / 100.0 * referenceSize.height) + areaSize.width *= scale + areaSize.height *= scale + let targetFrame = CGRect(x: coordinates.x / 100.0 * referenceSize.width - areaSize.width * 0.5, y: coordinates.y / 100.0 * referenceSize.height - areaSize.height * 0.5, width: areaSize.width, height: areaSize.height) + if targetFrame.width < 5.0 || targetFrame.height < 5.0 { + return nil + } + var cornerRadius: CGFloat = 0.0 + if let radius = coordinates.cornerRadius { + cornerRadius = radius / 100.0 * areaSize.width + } + + return (targetFrame, coordinates.rotation * (CGFloat.pi / 180.0), cornerRadius) + } + var nextId = 0 for mediaArea in story.mediaAreas { switch mediaArea { case let .reaction(coordinates, reaction, flags): - let referenceSize = size - var areaSize = CGSize(width: coordinates.width / 100.0 * referenceSize.width, height: coordinates.height / 100.0 * referenceSize.height) - areaSize.width *= 0.97 - areaSize.height *= 0.97 - let targetFrame = CGRect(x: coordinates.x / 100.0 * referenceSize.width - areaSize.width * 0.5, y: coordinates.y / 100.0 * referenceSize.height - areaSize.height * 0.5, width: areaSize.width, height: areaSize.height) - if targetFrame.width < 5.0 || targetFrame.height < 5.0 { + guard let (itemFrame, itemRotation, _) = getFrameAndRotation(coordinates: coordinates, scale: 0.97) else { continue } - let itemView: ItemView + let itemView: ReactionView let itemId = nextId - if let current = self.itemViews[itemId] { + if let current = self.itemViews[itemId] as? ReactionView { itemView = current } else { - itemView = ItemView(frame: CGRect()) + itemView = ReactionView(frame: CGRect()) itemView.activate = { [weak self] view, reaction in self?.activate?(view, reaction) } @@ -590,9 +738,9 @@ final class StoryItemOverlaysView: UIView { self.addSubview(itemView) } - transition.setPosition(view: itemView, position: targetFrame.center) - transition.setBounds(view: itemView, bounds: CGRect(origin: CGPoint(), size: targetFrame.size)) - transition.setTransform(view: itemView, transform: CATransform3DMakeRotation(coordinates.rotation * (CGFloat.pi / 180.0), 0.0, 0.0, 1.0)) + transition.setPosition(view: itemView, position: itemFrame.center) + transition.setBounds(view: itemView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) + transition.setTransform(view: itemView, transform: CATransform3DMakeRotation(itemRotation, 0.0, 0.0, 1.0)) var counter = 0 if let reactionData = story.views?.reactions.first(where: { $0.value == reaction }) { @@ -607,10 +755,43 @@ final class StoryItemOverlaysView: UIView { availableReactions: availableReactions, entityFiles: entityFiles, synchronous: attemptSynchronous, - size: targetFrame.size, + size: itemFrame.size, isActive: isActive ) + nextId += 1 + case let .weather(coordinates, emoji, temperature, color): + guard let (itemFrame, itemRotation, cornerRadius) = getFrameAndRotation(coordinates: coordinates) else { + continue + } + + let itemView: WeatherView + let itemId = nextId + if let current = self.itemViews[itemId] as? WeatherView { + itemView = current + } else { + itemView = WeatherView(frame: CGRect()) + itemView.isUserInteractionEnabled = false + self.itemViews[itemId] = itemView + self.addSubview(itemView) + } + + let itemSize = itemView.update( + context: context, + emoji: emoji, + emojiFile: context.animatedEmojiStickersValue[emoji]?.first?.file, + temperature: temperature, + color: color, + synchronous: attemptSynchronous, + size: itemFrame.size, + cornerRadius: cornerRadius, + isActive: isActive + ) + + transition.setPosition(view: itemView, position: itemFrame.center) + transition.setBounds(view: itemView, bounds: CGRect(origin: CGPoint(), size: itemSize)) + transition.setTransform(view: itemView, transform: CATransform3DMakeRotation(itemRotation, 0.0, 0.0, 1.0)) + nextId += 1 default: break diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 8f416abce8f..e4b726ed6c5 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -43,6 +43,7 @@ import TelegramUIPreferences import StoryFooterPanelComponent import TelegramNotices import SliderContextItem +import SaveProgressScreen public final class StoryAvailableReactions: Equatable { let reactionItems: [ReactionItem] @@ -118,6 +119,7 @@ public final class StoryItemSetContainerComponent: Component { public let navigate: (NavigationDirection) -> Void public let delete: () -> Void public let markAsSeen: (StoryId) -> Void + public let reorder: () -> Void public let controller: () -> ViewController? public let toggleAmbientMode: () -> Void public let keyboardInputData: Signal @@ -154,6 +156,7 @@ public final class StoryItemSetContainerComponent: Component { navigate: @escaping (NavigationDirection) -> Void, delete: @escaping () -> Void, markAsSeen: @escaping (StoryId) -> Void, + reorder: @escaping () -> Void, controller: @escaping () -> ViewController?, toggleAmbientMode: @escaping () -> Void, keyboardInputData: Signal, @@ -189,6 +192,7 @@ public final class StoryItemSetContainerComponent: Component { self.navigate = navigate self.delete = delete self.markAsSeen = markAsSeen + self.reorder = reorder self.controller = controller self.toggleAmbientMode = toggleAmbientMode self.keyboardInputData = keyboardInputData @@ -1657,6 +1661,7 @@ public final class StoryItemSetContainerComponent: Component { var isChannel = false var canShare = true var displayFooter = false + var displayFooterViews = true if case let .channel(channel) = component.slice.effectivePeer { isChannel = true if channel.addressName == nil { @@ -1678,6 +1683,9 @@ public final class StoryItemSetContainerComponent: Component { displayFooter = true } else if component.slice.item.storyItem.isPending { displayFooter = true + } else if case let .user(user) = component.slice.peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { + displayFooter = true + displayFooterViews = false } if component.slice.item.storyItem.isForwardingDisabled { canShare = false @@ -1749,6 +1757,7 @@ public final class StoryItemSetContainerComponent: Component { canViewChannelStats: component.slice.additionalPeerData.canViewStats, canShare: canShare, externalViews: nil, + displayViews: displayFooterViews, expandFraction: footerExpandFraction, expandViewStats: { [weak self] in guard let self, let component = self.component else { @@ -1771,42 +1780,11 @@ public final class StoryItemSetContainerComponent: Component { } }, deleteAction: { [weak self] in - guard let self, let component = self.component else { + guard let self else { return } - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - let actionSheet = ActionSheetController(presentationData: presentationData) - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: component.strings.Story_ContextDeleteStory, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let self, let component = self.component else { - return - } - component.delete() - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - - actionSheet.dismissed = { [weak self] _ in - guard let self else { - return - } - self.sendMessageContext.actionSheet = nil - self.updateIsProgressPaused() - } - self.sendMessageContext.actionSheet = actionSheet - self.updateIsProgressPaused() - - component.presentController(actionSheet, nil) + self.performDeleteAction() }, moreAction: { [weak self] sourceView, gesture in guard let self else { @@ -2775,6 +2753,9 @@ public final class StoryItemSetContainerComponent: Component { } else { showMessageInputPanel = component.slice.effectivePeer.id != component.context.account.peerId } + if case let .user(user) = component.slice.peer, let _ = user.botInfo { + showMessageInputPanel = false + } var isUnsupported = false var disabledPlaceholder: MessageInputPanelComponent.DisabledPlaceholder? @@ -3388,7 +3369,7 @@ public final class StoryItemSetContainerComponent: Component { guard let self else { return } - self.navigateToPeer(peer: peer, chat: true, subject: .message(id: .id(messageId), highlight: nil, timecode: nil)) + self.navigateToPeer(peer: peer, chat: true, subject: .message(id: .id(messageId), highlight: nil, timecode: nil, setupReply: false)) }, peerContextAction: { [weak self] peer, sourceView, gesture in guard let self, let component = self.component else { @@ -3771,36 +3752,40 @@ public final class StoryItemSetContainerComponent: Component { headerRightOffset -= 51.0 } - let moreButtonSize = self.moreButton.update( - transition: transition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent(LottieComponent( - content: LottieComponent.AppBundleContent( - name: "anim_story_more" - ), - color: .white, - startingPosition: .end, - size: CGSize(width: 30.0, height: 30.0) - )), - effectAlignment: .center, - minSize: CGSize(width: 33.0, height: 64.0), - action: { [weak self] in - guard let self else { - return - } - guard let moreButtonView = self.moreButton.view else { - return - } - if let animationView = (moreButtonView as? PlainButtonComponent.View)?.contentView as? LottieComponent.View { - animationView.playOnce() + var moreButtonSize: CGSize? + if case let .user(user) = component.slice.peer, let botInfo = user.botInfo, !botInfo.flags.contains(.canEdit) { + } else { + moreButtonSize = self.moreButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent( + name: "anim_story_more" + ), + color: .white, + startingPosition: .end, + size: CGSize(width: 30.0, height: 30.0) + )), + effectAlignment: .center, + minSize: CGSize(width: 33.0, height: 64.0), + action: { [weak self] in + guard let self else { + return + } + guard let moreButtonView = self.moreButton.view else { + return + } + if let animationView = (moreButtonView as? PlainButtonComponent.View)?.contentView as? LottieComponent.View { + animationView.playOnce() + } + self.performMoreAction(sourceView: moreButtonView, gesture: nil) } - self.performMoreAction(sourceView: moreButtonView, gesture: nil) - } - )), - environment: {}, - containerSize: CGSize(width: 33.0, height: 64.0) - ) - if let moreButtonView = self.moreButton.view { + )), + environment: {}, + containerSize: CGSize(width: 33.0, height: 64.0) + ) + } + if let moreButtonSize, let moreButtonView = self.moreButton.view { if moreButtonView.superview == nil { self.controlsClippingView.addSubview(moreButtonView) } @@ -3817,7 +3802,7 @@ public final class StoryItemSetContainerComponent: Component { isVideo = true soundAlpha = 1.0 for attribute in file.attributes { - if case let .Video(_, _, flags, _) = attribute { + if case let .Video(_, _, flags, _, _) = attribute { if flags.contains(.isSilent) { isSilentVideo = true soundAlpha = 0.5 @@ -3850,7 +3835,7 @@ public final class StoryItemSetContainerComponent: Component { var isSilentVideo = false if case let .file(file) = component.slice.item.storyItem.media { for attribute in file.attributes { - if case let .Video(_, _, flags, _) = attribute { + if case let .Video(_, _, flags, _, _) = attribute { if flags.contains(.isSilent) { isSilentVideo = true } @@ -4644,7 +4629,7 @@ public final class StoryItemSetContainerComponent: Component { presentationData: presentationData, content: .sticker(context: context, file: animation, loop: false, title: nil, text: component.strings.Story_ToastReactionSent, undoText: component.strings.Story_ToastViewInChat, customAction: { [weak self] in if let messageId = messageIds.first, let self { - self.navigateToPeer(peer: peer, chat: true, subject: messageId.flatMap { .message(id: .id($0), highlight: nil, timecode: nil) }) + self.navigateToPeer(peer: peer, chat: true, subject: messageId.flatMap { .message(id: .id($0), highlight: nil, timecode: nil, setupReply: false) }) } }), elevatedLayout: false, @@ -5356,7 +5341,7 @@ public final class StoryItemSetContainerComponent: Component { } private let updateDisposable = MetaDisposable() - func openStoryEditing(repost: Bool = false) { + func openStoryEditing(repost: Bool = false, cover: Bool = false) { guard let component = self.component else { return } @@ -5367,7 +5352,17 @@ public final class StoryItemSetContainerComponent: Component { var videoPlaybackPosition: Double? if let visibleItem = self.visibleItems[component.slice.item.id], let view = visibleItem.view.view as? StoryItemContentComponent.View { - videoPlaybackPosition = view.videoPlaybackPosition + if cover { + if case let .file(file) = component.slice.item.storyItem.media { + for attribute in file.attributes { + if case let .Video(_, _, _, _, coverTime) = attribute { + videoPlaybackPosition = coverTime + } + } + } + } else { + videoPlaybackPosition = view.videoPlaybackPosition + } } guard let controller = MediaEditorScreen.makeEditStoryController( @@ -5375,6 +5370,7 @@ public final class StoryItemSetContainerComponent: Component { peer: component.slice.effectivePeer, storyItem: component.slice.item.storyItem, videoPlaybackPosition: videoPlaybackPosition, + cover: cover, repost: repost, transitionIn: .noAnimation, transitionOut: nil, @@ -5650,6 +5646,60 @@ public final class StoryItemSetContainerComponent: Component { } } + private func performDeleteAction() { + guard let component = self.component else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) + + let deleteTitle: String + if case let .user(user) = component.slice.peer, user.botInfo != nil { + deleteTitle = component.strings.BotPreview_ViewContextDelete + } else { + deleteTitle = component.strings.Story_ContextDeleteStory + } + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: deleteTitle, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let self, let component = self.component else { + return + } + component.delete() + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + + actionSheet.dismissed = { [weak self] _ in + guard let self else { + return + } + self.sendMessageContext.actionSheet = nil + self.updateIsProgressPaused() + } + self.sendMessageContext.actionSheet = actionSheet + self.updateIsProgressPaused() + + component.presentController(actionSheet, nil) + } + + private func performReorderAction() { + guard let component = self.component else { + return + } + + component.reorder() + } + private func performLikeAction() { guard let component = self.component else { return @@ -6068,6 +6118,19 @@ public final class StoryItemSetContainerComponent: Component { self.openStoryEditing() }))) + if case .file = component.slice.item.storyItem.media, component.slice.item.storyItem.isPinned { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_EditCover, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Stories/Context Menu/EditCover"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.openStoryEditing(cover: true) + }))) + } + items.append(.separator) items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? component.strings.Story_Context_RemoveFromProfile : component.strings.Story_Context_SaveToProfile, icon: { theme in @@ -6252,6 +6315,19 @@ public final class StoryItemSetContainerComponent: Component { } self.openStoryEditing() }))) + + if case .file = component.slice.item.storyItem.media, component.slice.item.storyItem.isPinned { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_EditCover, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Stories/Context Menu/EditCover"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.openStoryEditing(cover: true) + }))) + } } if !items.isEmpty { @@ -6526,346 +6602,372 @@ public final class StoryItemSetContainerComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) var items: [ContextMenuItem] = [] - if case .file = component.slice.item.storyItem.media { - var speedValue: String = presentationData.strings.PlaybackSpeed_Normal - var speedIconText: String = "1x" - var didSetSpeedValue = false - for (text, iconText, speed) in speedList(strings: presentationData.strings) { - if abs(speed - baseRate) < 0.01 { - speedValue = text - speedIconText = iconText - didSetSpeedValue = true - break - } - } - if !didSetSpeedValue && baseRate != 1.0 { - speedValue = String(format: "%.1fx", baseRate) - speedIconText = speedValue + if case let .user(user) = component.slice.peer, let botInfo = user.botInfo { + if botInfo.flags.contains(.canEdit) { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.BotPreviews_MenuReorder, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { + return + } + + component.reorder() + }))) + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.performDeleteAction() + }))) } - - items.append(.action(ContextMenuActionItem(text: presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in - return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor) - }, action: { [weak self] c, _ in - guard let self else { - c?.dismiss(completion: nil) - return - } - - c?.pushItems(items: self.contextMenuSpeedItems(value: baseRatePromise) |> map { ContextController.Items(content: .list($0)) }) - }))) - items.append(.separator) - } - - let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: component.slice.effectivePeer._asPeer(), peerSettings: settings._asNotificationSettings(), topSearchPeers: topSearchPeers) - - if !component.slice.effectivePeer.isService && isContact { - items.append(.action(ContextMenuActionItem(text: isMuted ? component.strings.StoryFeed_ContextNotifyOn : component.strings.StoryFeed_ContextNotifyOff, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: component.slice.additionalPeerData.isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self, let component = self.component else { - return - } - - let _ = component.context.engine.peers.togglePeerStoriesMuted(peerId: component.slice.effectivePeer.id).startStandalone() - - let iconColor = UIColor.white - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - if isMuted { - self.component?.presentController(UndoOverlayController( - presentationData: presentationData, - content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [ - "Middle.Group 1.Fill 1": iconColor, - "Top.Group 1.Fill 1": iconColor, - "Bottom.Group 1.Fill 1": iconColor, - "EXAMPLE.Group 1.Fill 1": iconColor, - "Line.Group 1.Stroke 1": iconColor - ], title: nil, text: component.strings.StoryFeed_TooltipNotifyOn(component.slice.effectivePeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, customUndoText: nil, timeout: nil), - elevatedLayout: false, - animateInAsReplacement: false, - blurred: true, - action: { _ in return false } - ), nil) - } else { - self.component?.presentController(UndoOverlayController( - presentationData: presentationData, - content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [ - "Middle.Group 1.Fill 1": iconColor, - "Top.Group 1.Fill 1": iconColor, - "Bottom.Group 1.Fill 1": iconColor, - "EXAMPLE.Group 1.Fill 1": iconColor, - "Line.Group 1.Stroke 1": iconColor - ], title: nil, text: component.strings.StoryFeed_TooltipNotifyOff(component.slice.effectivePeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, customUndoText: nil, timeout: nil), - elevatedLayout: false, - animateInAsReplacement: false, - blurred: true, - action: { _ in return false } - ), nil) + } else { + if case .file = component.slice.item.storyItem.media { + var speedValue: String = presentationData.strings.PlaybackSpeed_Normal + var speedIconText: String = "1x" + var didSetSpeedValue = false + for (text, iconText, speed) in speedList(strings: presentationData.strings) { + if abs(speed - baseRate) < 0.01 { + speedValue = text + speedIconText = iconText + didSetSpeedValue = true + break + } } - }))) - } - - if !component.slice.effectivePeer.isService && component.slice.item.storyItem.isPublic && (component.slice.effectivePeer.addressName != nil || !component.slice.effectivePeer._asPeer().usernames.isEmpty) { - items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_CopyLink, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self, let component = self.component else { - return + if !didSetSpeedValue && baseRate != 1.0 { + speedValue = String(format: "%.1fx", baseRate) + speedIconText = speedValue } - let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id) - |> deliverOnMainQueue).startStandalone(next: { [weak self] link in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in + return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + guard let self else { + c?.dismiss(completion: nil) + return + } + + c?.pushItems(items: self.contextMenuSpeedItems(value: baseRatePromise) |> map { ContextController.Items(content: .list($0)) }) + }))) + items.append(.separator) + } + + let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: component.slice.effectivePeer._asPeer(), peerSettings: settings._asNotificationSettings(), topSearchPeers: topSearchPeers) + + if !component.slice.effectivePeer.isService && isContact { + items.append(.action(ContextMenuActionItem(text: isMuted ? component.strings.StoryFeed_ContextNotifyOn : component.strings.StoryFeed_ContextNotifyOff, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: component.slice.additionalPeerData.isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + guard let self, let component = self.component else { return } - if let link { - UIPasteboard.general.string = link - - component.presentController(UndoOverlayController( + + let _ = component.context.engine.peers.togglePeerStoriesMuted(peerId: component.slice.effectivePeer.id).startStandalone() + + let iconColor = UIColor.white + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + if isMuted { + self.component?.presentController(UndoOverlayController( presentationData: presentationData, - content: .linkCopied(text: component.strings.Story_ToastLinkCopied), + content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [ + "Middle.Group 1.Fill 1": iconColor, + "Top.Group 1.Fill 1": iconColor, + "Bottom.Group 1.Fill 1": iconColor, + "EXAMPLE.Group 1.Fill 1": iconColor, + "Line.Group 1.Stroke 1": iconColor + ], title: nil, text: component.strings.StoryFeed_TooltipNotifyOn(component.slice.effectivePeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, customUndoText: nil, timeout: nil), + elevatedLayout: false, + animateInAsReplacement: false, + blurred: true, + action: { _ in return false } + ), nil) + } else { + self.component?.presentController(UndoOverlayController( + presentationData: presentationData, + content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [ + "Middle.Group 1.Fill 1": iconColor, + "Top.Group 1.Fill 1": iconColor, + "Bottom.Group 1.Fill 1": iconColor, + "EXAMPLE.Group 1.Fill 1": iconColor, + "Line.Group 1.Stroke 1": iconColor + ], title: nil, text: component.strings.StoryFeed_TooltipNotifyOff(component.slice.effectivePeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, blurred: true, action: { _ in return false } ), nil) } - }) - }))) - } - - if !component.slice.item.storyItem.isMy, case let .file(file) = component.slice.item.storyItem.media, file.isVideo { - let isHq = component.slice.additionalPeerData.preferHighQualityStories - items.append(.action(ContextMenuActionItem(text: isHq ? component.strings.Story_ContextMenuSD : component.strings.Story_ContextMenuHD, icon: { theme in - if isHq { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QualitySd"), color: theme.contextMenu.primaryColor) - } else { - return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/QualityHd" : "Chat/Context Menu/QualityHdLocked"), color: theme.contextMenu.primaryColor) - } - }, action: { [weak self] _, a in - a(.default) - - guard let self, let component = self.component, let controller = component.controller() else { - return - } - - if !component.slice.additionalPeerData.preferHighQualityStories && !accountUser.isPremium { - self.presentQualityUpgradeScreen() - - return - } - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - let title: String - let text: String - if component.slice.additionalPeerData.preferHighQualityStories { - title = component.strings.Story_ToastQualitySD_Title - text = component.strings.Story_ToastQualitySD_Text - } else { - title = component.strings.Story_ToastQualityHD_Title - text = component.strings.Story_ToastQualityHD_Text - } - controller.present(UndoOverlayController( - presentationData: presentationData, - content: .info(title: title, text: text, timeout: nil, customUndoText: nil), - elevatedLayout: false, - animateInAsReplacement: false, - blurred: true, - action: { _ in return false } - ), in: .current) - - let _ = updateMediaDownloadSettingsInteractively(accountManager: component.context.sharedContext.accountManager, { settings in - var settings = settings - settings.highQualityStories = !isHq - return settings - }).startStandalone() - }))) - } - - var isHidden = false - if case let .user(user) = component.slice.effectivePeer, let storiesHidden = user.storiesHidden { - isHidden = storiesHidden - } else if case let .channel(channel) = component.slice.effectivePeer, let storiesHidden = channel.storiesHidden { - isHidden = storiesHidden - } - - var canArchive = false - if isHidden { - canArchive = true - } else { - if case .user = component.slice.effectivePeer, !component.slice.effectivePeer.isService { - canArchive = true - } else if case .channel = component.slice.effectivePeer { - canArchive = true + }))) } - } - - if canArchive { - items.append(.action(ContextMenuActionItem(text: isHidden ? component.strings.StoryFeed_ContextUnarchive : component.strings.StoryFeed_ContextArchive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: isHidden ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self, let component = self.component else { - return - } - - let _ = component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.effectivePeer.id, isHidden: !isHidden) - - let text = !isHidden ? component.strings.StoryFeed_TooltipArchive(component.slice.effectivePeer.compactDisplayTitle).string : component.strings.StoryFeed_TooltipUnarchive(component.slice.effectivePeer.compactDisplayTitle).string - let tooltipScreen = TooltipScreen( - context: component.context, - account: component.context.account, - sharedContext: component.context.sharedContext, - text: .markdown(text: text), - style: .customBlur(UIColor(rgb: 0x1c1c1c), 0.0), - icon: .peer(peer: component.slice.effectivePeer, isStory: true), - action: TooltipScreen.Action( - title: component.strings.Undo_Undo, - action: { - component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.effectivePeer.id, isHidden: isHidden) + + if !component.slice.effectivePeer.isService && component.slice.item.storyItem.isPublic && (component.slice.effectivePeer.addressName != nil || !component.slice.effectivePeer._asPeer().usernames.isEmpty) { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_CopyLink, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { + return + } + + let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id) + |> deliverOnMainQueue).startStandalone(next: { [weak self] link in + guard let self, let component = self.component else { + return } - ), - location: .bottom, - shouldDismissOnTouch: { _, _ in return .dismiss(consume: false) } - ) - tooltipScreen.willBecomeDismissed = { [weak self] _ in - guard let self else { + if let link { + UIPasteboard.general.string = link + + component.presentController(UndoOverlayController( + presentationData: presentationData, + content: .linkCopied(text: component.strings.Story_ToastLinkCopied), + elevatedLayout: false, + animateInAsReplacement: false, + blurred: true, + action: { _ in return false } + ), nil) + } + }) + }))) + } + + if !component.slice.item.storyItem.isMy, case let .file(file) = component.slice.item.storyItem.media, file.isVideo { + let isHq = component.slice.additionalPeerData.preferHighQualityStories + items.append(.action(ContextMenuActionItem(text: isHq ? component.strings.Story_ContextMenuSD : component.strings.Story_ContextMenuHD, icon: { theme in + if isHq { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QualitySd"), color: theme.contextMenu.primaryColor) + } else { + return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/QualityHd" : "Chat/Context Menu/QualityHdLocked"), color: theme.contextMenu.primaryColor) + } + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component, let controller = component.controller() else { return } - self.sendMessageContext.tooltipScreen = nil - self.updateIsProgressPaused() - } - self.sendMessageContext.tooltipScreen?.dismiss() - self.sendMessageContext.tooltipScreen = tooltipScreen - self.updateIsProgressPaused() - component.controller()?.present(tooltipScreen, in: .current) - }))) - } - - if !component.slice.item.storyItem.isForwardingDisabled { - let saveText: String = component.strings.Story_Context_SaveToGallery - items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Download" : "Chat/Context Menu/DownloadLocked"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - - if accountUser.isPremium { - self.requestSave() - } else { - self.presentSaveUpgradeScreen() - } - }))) - } - - if case .user = component.slice.effectivePeer { - items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextStealthMode, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - if accountUser.isPremium { - self.sendMessageContext.requestStealthMode(view: self) - } else { - self.presentStealthModeUpgradeScreen() - } - }))) - } - - if component.slice.additionalPeerData.canViewStats { - items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_ViewStats, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self, let component = self.component else { - return - } - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) - let statsController = component.context.sharedContext.makeStoryStatsController( - context: component.context, - updatedPresentationData: (presentationData, .single(presentationData)), - peerId: component.slice.effectivePeer.id, - storyId: component.slice.item.storyItem.id, - storyItem: component.slice.item.storyItem, - fromStory: true - ) - component.controller()?.push(statsController) - }))) - } - - if !component.slice.item.storyItem.text.isEmpty { - let (canTranslate, _) = canTranslateText(context: component.context, text: component.slice.item.storyItem.text, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: false, ignoredLanguages: translationSettings.ignoredLanguages) - if canTranslate { - items.append(.action(ContextMenuActionItem(text: component.strings.Conversation_ContextMenuTranslate, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.default) - guard let self, let component = self.component else { + if !component.slice.additionalPeerData.preferHighQualityStories && !accountUser.isPremium { + self.presentQualityUpgradeScreen() + return } - self.sendMessageContext.performTranslateTextAction(view: self, text: component.slice.item.storyItem.text, entities: component.slice.item.storyItem.entities) + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + let title: String + let text: String + if component.slice.additionalPeerData.preferHighQualityStories { + title = component.strings.Story_ToastQualitySD_Title + text = component.strings.Story_ToastQualitySD_Text + } else { + title = component.strings.Story_ToastQualityHD_Title + text = component.strings.Story_ToastQualityHD_Text + } + controller.present(UndoOverlayController( + presentationData: presentationData, + content: .info(title: title, text: text, timeout: nil, customUndoText: nil), + elevatedLayout: false, + animateInAsReplacement: false, + blurred: true, + action: { _ in return false } + ), in: .current) + + let _ = updateMediaDownloadSettingsInteractively(accountManager: component.context.sharedContext.accountManager, { settings in + var settings = settings + settings.highQualityStories = !isHq + return settings + }).startStandalone() }))) } - } - - if !component.slice.effectivePeer.isService { - items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Report, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] c, a in - guard let self, let component = self.component, let controller = component.controller() else { - return + + var isHidden = false + if case let .user(user) = component.slice.effectivePeer, let storiesHidden = user.storiesHidden { + isHidden = storiesHidden + } else if case let .channel(channel) = component.slice.effectivePeer, let storiesHidden = channel.storiesHidden { + isHidden = storiesHidden + } + + var canArchive = false + if isHidden { + canArchive = true + } else { + if case .user = component.slice.effectivePeer, !component.slice.effectivePeer.isService { + canArchive = true + } else if case .channel = component.slice.effectivePeer { + canArchive = true } - - let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other] - presentPeerReportOptions( - context: component.context, - parent: controller, - contextController: c, - backAction: { _ in }, - subject: .story(component.slice.effectivePeer.id, component.slice.item.storyItem.id), - options: options, - passthrough: true, - forceTheme: defaultDarkPresentationTheme, - isDetailedReportingVisible: { [weak self] isReporting in + } + + if canArchive { + items.append(.action(ContextMenuActionItem(text: isHidden ? component.strings.StoryFeed_ContextUnarchive : component.strings.StoryFeed_ContextArchive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: isHidden ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { + return + } + + let _ = component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.effectivePeer.id, isHidden: !isHidden) + + let text = !isHidden ? component.strings.StoryFeed_TooltipArchive(component.slice.effectivePeer.compactDisplayTitle).string : component.strings.StoryFeed_TooltipUnarchive(component.slice.effectivePeer.compactDisplayTitle).string + let tooltipScreen = TooltipScreen( + context: component.context, + account: component.context.account, + sharedContext: component.context.sharedContext, + text: .markdown(text: text), + style: .customBlur(UIColor(rgb: 0x1c1c1c), 0.0), + icon: .peer(peer: component.slice.effectivePeer, isStory: true), + action: TooltipScreen.Action( + title: component.strings.Undo_Undo, + action: { + component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.effectivePeer.id, isHidden: isHidden) + } + ), + location: .bottom, + shouldDismissOnTouch: { _, _ in return .dismiss(consume: false) } + ) + tooltipScreen.willBecomeDismissed = { [weak self] _ in guard let self else { return } - self.isReporting = isReporting + self.sendMessageContext.tooltipScreen = nil self.updateIsProgressPaused() - }, - completion: { [weak self] reason, _ in - guard let self, let component = self.component, let controller = component.controller(), let reason else { + } + self.sendMessageContext.tooltipScreen?.dismiss() + self.sendMessageContext.tooltipScreen = tooltipScreen + self.updateIsProgressPaused() + component.controller()?.present(tooltipScreen, in: .current) + }))) + } + + if !component.slice.item.storyItem.isForwardingDisabled { + let saveText: String = component.strings.Story_Context_SaveToGallery + items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Download" : "Chat/Context Menu/DownloadLocked"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + + if accountUser.isPremium { + self.requestSave() + } else { + self.presentSaveUpgradeScreen() + } + }))) + } + + if case .user = component.slice.effectivePeer { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextStealthMode, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + if accountUser.isPremium { + self.sendMessageContext.requestStealthMode(view: self) + } else { + self.presentStealthModeUpgradeScreen() + } + }))) + } + + if component.slice.additionalPeerData.canViewStats { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_ViewStats, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { + return + } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + let statsController = component.context.sharedContext.makeStoryStatsController( + context: component.context, + updatedPresentationData: (presentationData, .single(presentationData)), + peerId: component.slice.effectivePeer.id, + storyId: component.slice.item.storyItem.id, + storyItem: component.slice.item.storyItem, + fromStory: true + ) + component.controller()?.push(statsController) + }))) + } + + if !component.slice.item.storyItem.text.isEmpty { + let (canTranslate, _) = canTranslateText(context: component.context, text: component.slice.item.storyItem.text, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: false, ignoredLanguages: translationSettings.ignoredLanguages) + if canTranslate { + items.append(.action(ContextMenuActionItem(text: component.strings.Conversation_ContextMenuTranslate, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + f(.default) + + guard let self, let component = self.component else { return } - let _ = component.context.engine.peers.reportPeerStory(peerId: component.slice.effectivePeer.id, storyId: component.slice.item.storyItem.id, reason: reason, message: "").startStandalone() - controller.present( - UndoOverlayController( - presentationData: presentationData, - content: .emoji( - name: "PoliceCar", - text: presentationData.strings.Report_Succeed - ), - elevatedLayout: false, - blurred: true, - action: { _ in return false } - ) - , in: .current - ) + self.sendMessageContext.performTranslateTextAction(view: self, text: component.slice.item.storyItem.text, entities: component.slice.item.storyItem.entities) + }))) + } + } + + if !component.slice.effectivePeer.isService { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Report, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, a in + guard let self, let component = self.component, let controller = component.controller() else { + return } - ) - }))) + + let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other] + presentPeerReportOptions( + context: component.context, + parent: controller, + contextController: c, + backAction: { _ in }, + subject: .story(component.slice.effectivePeer.id, component.slice.item.storyItem.id), + options: options, + passthrough: true, + forceTheme: defaultDarkPresentationTheme, + isDetailedReportingVisible: { [weak self] isReporting in + guard let self else { + return + } + self.isReporting = isReporting + self.updateIsProgressPaused() + }, + completion: { [weak self] reason, _ in + guard let self, let component = self.component, let controller = component.controller(), let reason else { + return + } + let _ = component.context.engine.peers.reportPeerStory(peerId: component.slice.effectivePeer.id, storyId: component.slice.item.storyItem.id, reason: reason, message: "").startStandalone() + controller.present( + UndoOverlayController( + presentationData: presentationData, + content: .emoji( + name: "PoliceCar", + text: presentationData.strings.Report_Succeed + ), + elevatedLayout: false, + blurred: true, + action: { _ in return false } + ) + , in: .current + ) + } + ) + }))) + } } let (tip, tipSignal) = self.getLinkedStickerPacks() diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 279e2bee0d4..e7a36f29aec 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -377,7 +377,7 @@ final class StoryItemSetContainerSendMessage { animateInAsReplacement: false, action: { [weak view, weak self] action in if case .undo = action, let messageId { - view?.navigateToPeer(peer: peer, chat: true, subject: isScheduled ? .scheduledMessages : .message(id: .id(messageId), highlight: nil, timecode: nil)) + view?.navigateToPeer(peer: peer, chat: true, subject: isScheduled ? .scheduledMessages : .message(id: .id(messageId), highlight: nil, timecode: nil, setupReply: false)) } self?.tooltipScreen = nil view?.updateIsProgressPaused() @@ -1808,7 +1808,7 @@ final class StoryItemSetContainerSendMessage { //TODO:gift controller break case let .app(bot): - let params = WebAppParameters(source: .attachMenu, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: true) + let params = WebAppParameters(source: .attachMenu, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: true) let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) let controller = WebAppController(context: component.context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil, threadId: nil) @@ -2768,7 +2768,7 @@ final class StoryItemSetContainerSendMessage { return } if let navigationController = controller.navigationController as? NavigationController { - component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil))) + component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) } completion?() }) @@ -3434,6 +3434,8 @@ final class StoryItemSetContainerSendMessage { actions.append(ContextMenuAction(content: .textWithSubtitleAndIcon(title: updatedPresentationData.initial.strings.Story_ViewLink, subtitle: url, icon: generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: .white)), action: { action() })) + case .weather: + return } self.selectedMediaArea = mediaArea diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 4c5d2bbb501..97f8d32d356 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -45,6 +45,7 @@ public final class StoryFooterPanelComponent: Component { public let canViewChannelStats: Bool public let canShare: Bool public let externalViews: EngineStoryItem.Views? + public let displayViews: Bool public let expandFraction: CGFloat public let expandViewStats: () -> Void public let deleteAction: () -> Void @@ -65,6 +66,7 @@ public final class StoryFooterPanelComponent: Component { canViewChannelStats: Bool, canShare: Bool, externalViews: EngineStoryItem.Views?, + displayViews: Bool, expandFraction: CGFloat, expandViewStats: @escaping () -> Void, deleteAction: @escaping () -> Void, @@ -84,6 +86,7 @@ public final class StoryFooterPanelComponent: Component { self.canViewChannelStats = canViewChannelStats self.canShare = canShare self.externalViews = externalViews + self.displayViews = displayViews self.expandViewStats = expandViewStats self.expandFraction = expandFraction self.deleteAction = deleteAction @@ -122,6 +125,9 @@ public final class StoryFooterPanelComponent: Component { if lhs.externalViews != rhs.externalViews { return false } + if lhs.displayViews != rhs.displayViews { + return false + } if lhs.expandFraction != rhs.expandFraction { return false } @@ -413,7 +419,9 @@ public final class StoryFooterPanelComponent: Component { } let viewPart: String - if component.isChannel { + if !component.displayViews { + viewPart = "" + } else if component.isChannel { viewPart = "" } else if viewCount == 0 { viewPart = component.strings.Story_Footer_NoViews diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index 2a2536c59cc..dc2815c8c02 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -22,11 +22,13 @@ public final class TabSelectorComponent: Component { public var font: UIFont public var spacing: CGFloat public var lineSelection: Bool + public var verticalInset: CGFloat - public init(font: UIFont, spacing: CGFloat, lineSelection: Bool = false) { + public init(font: UIFont, spacing: CGFloat, lineSelection: Bool = false, verticalInset: CGFloat = 0.0) { self.font = font self.spacing = spacing self.lineSelection = lineSelection + self.verticalInset = verticalInset } } @@ -92,7 +94,7 @@ public final class TabSelectorComponent: Component { } } - public final class View: UIView { + public final class View: UIScrollView { private var component: TabSelectorComponent? private weak var state: EmptyComponentState? @@ -104,6 +106,15 @@ public final class TabSelectorComponent: Component { super.init(frame: frame) + self.showsVerticalScrollIndicator = false + self.showsHorizontalScrollIndicator = false + self.scrollsToTop = false + self.delaysContentTouches = false + self.canCancelContentTouches = true + self.contentInsetAdjustmentBehavior = .never + self.alwaysBounceVertical = false + self.clipsToBounds = false + self.addSubview(self.selectionView) } @@ -114,6 +125,10 @@ public final class TabSelectorComponent: Component { deinit { } + override public func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + func update(component: TabSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let selectionColorUpdated = component.colors.selection != self.component?.colors.selection @@ -121,6 +136,12 @@ public final class TabSelectorComponent: Component { self.state = state let baseHeight: CGFloat = 28.0 + + var verticalInset: CGFloat = 0.0 + if let customLayout = component.customLayout { + verticalInset = customLayout.verticalInset * 2.0 + } + let innerInset: CGFloat = 12.0 let spacing: CGFloat = component.customLayout?.spacing ?? 2.0 @@ -148,7 +169,7 @@ public final class TabSelectorComponent: Component { } } - var contentWidth: CGFloat = 0.0 + var contentWidth: CGFloat = spacing var previousBackgroundRect: CGRect? var selectedBackgroundRect: CGRect? var nextBackgroundRect: CGRect? @@ -213,8 +234,8 @@ public final class TabSelectorComponent: Component { if !contentWidth.isZero { contentWidth += spacing } - let itemTitleFrame = CGRect(origin: CGPoint(x: contentWidth + innerInset, y: floor((baseHeight - itemSize.height) * 0.5)), size: itemSize) - let itemBackgroundRect = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: CGSize(width: innerInset + itemSize.width + innerInset, height: baseHeight)) + let itemTitleFrame = CGRect(origin: CGPoint(x: contentWidth + innerInset, y: verticalInset + floor((baseHeight - itemSize.height) * 0.5)), size: itemSize) + let itemBackgroundRect = CGRect(origin: CGPoint(x: contentWidth, y: verticalInset), size: CGSize(width: innerInset + itemSize.width + innerInset, height: baseHeight)) contentWidth = itemBackgroundRect.maxX if item.id == component.selectedId { @@ -237,6 +258,7 @@ public final class TabSelectorComponent: Component { } index += 1 } + contentWidth += spacing var removeIds: [AnyHashable] = [] for (id, itemView) in self.visibleItems { @@ -277,7 +299,14 @@ public final class TabSelectorComponent: Component { self.selectionView.alpha = 0.0 } - return CGSize(width: contentWidth, height: baseHeight) + self.contentSize = CGSize(width: contentWidth, height: baseHeight + verticalInset * 2.0) + self.disablesInteractiveTransitionGestureRecognizer = contentWidth > availableSize.width + + if let selectedBackgroundRect { + self.scrollRectToVisible(selectedBackgroundRect.insetBy(dx: -spacing, dy: 0.0), animated: false) + } + + return CGSize(width: min(contentWidth, availableSize.width), height: baseHeight + verticalInset * 2.0) } } diff --git a/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift b/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift index 953953dafa7..9b5bacdb479 100644 --- a/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift +++ b/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift @@ -171,4 +171,34 @@ public final class TextLoadingEffectView: UIView { self.updateAnimations(size: maskFrame.size) } } + + public func update(color: UIColor, size: CGSize, rects: [CGRect]) { + let rectsSet: [CGRect] = rects + + let maskFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -4.0, dy: -4.0) + + self.maskContentsView.backgroundColor = color.withAlphaComponent(0.1) + self.maskBorderContentsView.backgroundColor = color.withAlphaComponent(0.12) + + self.backgroundView.tintColor = color + self.borderBackgroundView.tintColor = color + + self.maskContentsView.frame = maskFrame + self.maskBorderContentsView.frame = maskFrame + + self.maskHighlightNode.updateRects(rectsSet) + self.maskHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize()) + + self.maskBorderHighlightNode.updateRects(rectsSet) + self.maskBorderHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize()) + + if self.size != maskFrame.size { + self.size = maskFrame.size + + self.backgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height)) + self.borderBackgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height)) + + self.updateAnimations(size: maskFrame.size) + } + } } diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/BUILD b/submodules/TelegramUI/Components/VideoMessageCameraScreen/BUILD index 0a630c430b1..04d9ac78aa6 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/BUILD +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/BUILD @@ -41,6 +41,7 @@ swift_library( "//submodules/TelegramAudio", "//submodules/ChatSendMessageActionUI", "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/LottieComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index b5c56fcbc31..39b2d9e9fbe 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -32,6 +32,7 @@ import LegacyMediaPickerUI import TelegramAudio import ChatSendMessageActionUI import ChatControllerInteraction +import LottieComponent struct CameraState: Equatable { enum Recording: Equatable { @@ -39,27 +40,63 @@ struct CameraState: Equatable { case holding case handsFree } + enum FlashTint: Equatable { + case white + case yellow + case blue + + var color: UIColor { + switch self { + case .white: + return .white + case .yellow: + return UIColor(rgb: 0xffed8c) + case .blue: + return UIColor(rgb: 0x8cdfff) + } + } + } let position: Camera.Position + let flashMode: Camera.FlashMode + let flashModeDidChange: Bool + let flashTint: FlashTint + let flashTintSize: CGFloat let recording: Recording let duration: Double let isDualCameraEnabled: Bool let isViewOnceEnabled: Bool func updatedPosition(_ position: Camera.Position) -> CameraState { - return CameraState(position: position, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) + return CameraState(position: position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) + } + + func updatedFlashMode(_ flashMode: Camera.FlashMode) -> CameraState { + return CameraState(position: self.position, flashMode: flashMode, flashModeDidChange: self.flashMode != flashMode, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) + } + + func updatedFlashTint(_ flashTint: FlashTint) -> CameraState { + return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) + } + + func updatedFlashTintSize(_ flashTintSize: CGFloat) -> CameraState { + return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) } func updatedRecording(_ recording: Recording) -> CameraState { - return CameraState(position: self.position, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) + var flashModeDidChange = self.flashModeDidChange + if case .none = self.recording { + flashModeDidChange = false + } + return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) } func updatedDuration(_ duration: Double) -> CameraState { - return CameraState(position: self.position, recording: self.recording, duration: duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) + return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) } func updatedIsViewOnceEnabled(_ isViewOnceEnabled: Bool) -> CameraState { - return CameraState(position: self.position, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: isViewOnceEnabled) + return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: isViewOnceEnabled) } } @@ -91,6 +128,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { let push: (ViewController) -> Void let startRecording: ActionSlot let stopRecording: ActionSlot + let cancelRecording: ActionSlot let completion: ActionSlot init( @@ -105,6 +143,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { push: @escaping (ViewController) -> Void, startRecording: ActionSlot, stopRecording: ActionSlot, + cancelRecording: ActionSlot, completion: ActionSlot ) { self.context = context @@ -118,6 +157,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { self.push = push self.startRecording = startRecording self.stopRecording = stopRecording + self.cancelRecording = cancelRecording self.completion = completion } @@ -146,7 +186,9 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { final class State: ComponentState { enum ImageKey: Hashable { case flip + case flash case buttonBackground + case flashImage } private var cachedImages: [ImageKey: UIImage] = [:] func image(_ key: ImageKey, theme: PresentationTheme) -> UIImage { @@ -157,9 +199,23 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { switch key { case .flip: image = UIImage(bundleImageName: "Camera/VideoMessageFlip")!.withRenderingMode(.alwaysTemplate) + case .flash: + image = UIImage(bundleImageName: "Camera/VideoMessageFlash")!.withRenderingMode(.alwaysTemplate) case .buttonBackground: let innerSize = CGSize(width: 40.0, height: 40.0) image = generateFilledCircleImage(diameter: innerSize.width, color: theme.rootController.navigationBar.opaqueBackgroundColor, strokeColor: theme.chat.inputPanel.panelSeparatorColor, strokeWidth: 0.5, backgroundColor: nil)! + case .flashImage: + image = generateImage(CGSize(width: 393.0, height: 852.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + var locations: [CGFloat] = [0.0, 0.2, 0.6, 1.0] + let colors: [CGColor] = [UIColor(rgb: 0xffffff, alpha: 0.25).cgColor, UIColor(rgb: 0xffffff, alpha: 0.25).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0 - 10.0) + context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: size.width, options: .drawsAfterEndLocation) + })!.withRenderingMode(.alwaysTemplate) } cachedImages[key] = image return image @@ -170,6 +226,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { private let present: (ViewController) -> Void private let startRecording: ActionSlot private let stopRecording: ActionSlot + private let cancelRecording: ActionSlot private let completion: ActionSlot private let getController: () -> VideoMessageCameraScreen? @@ -178,6 +235,8 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { var cameraState: CameraState? var didDisplayViewOnce = false + + var displayingFlashTint = false private let hapticFeedback = HapticFeedback() @@ -186,6 +245,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { present: @escaping (ViewController) -> Void, startRecording: ActionSlot, stopRecording: ActionSlot, + cancelRecording: ActionSlot, completion: ActionSlot, getController: @escaping () -> VideoMessageCameraScreen? = { return nil @@ -195,6 +255,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { self.present = present self.startRecording = startRecording self.stopRecording = stopRecording + self.cancelRecording = cancelRecording self.completion = completion self.getController = getController @@ -212,6 +273,10 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { self.stopRecording.connect({ [weak self] _ in self?.stopVideoRecording() }) + + self.cancelRecording.connect({ [weak self] _ in + self?.cancelVideoRecording() + }) } deinit { @@ -236,9 +301,100 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { } self.lastFlipTimestamp = currentTimestamp + let isFrontCamera = controller.cameraState.position == .back camera.togglePosition() - + self.hapticFeedback.impact(.veryLight) + + self.updateScreenBrightness(isFrontCamera: isFrontCamera) + + if isFrontCamera { + camera.setTorchActive(false) + } else { + camera.setTorchActive(controller.cameraState.flashMode == .on) + } + } + + func toggleFlashMode() { + guard let controller = self.getController(), let camera = controller.camera else { + return + } + var isFlashOn = false + switch controller.cameraState.flashMode { + case .off: + isFlashOn = true + camera.setFlashMode(.on) + case .on: + camera.setFlashMode(.off) + default: + camera.setFlashMode(.off) + } + self.hapticFeedback.impact(.light) + + self.updateScreenBrightness(isFlashOn: isFlashOn) + + if controller.cameraState.position == .back { + if isFlashOn { + camera.setTorchActive(true) + } else { + camera.setTorchActive(false) + } + } + } + + private var initialBrightness: CGFloat? + private var brightnessArguments: (Double, Double, CGFloat, CGFloat)? + private var brightnessAnimator: ConstantDisplayLinkAnimator? + + func updateScreenBrightness(isFrontCamera: Bool? = nil, isFlashOn: Bool? = nil) { + guard let controller = self.getController() else { + return + } + let isFrontCamera = isFrontCamera ?? (controller.cameraState.position == .front) + let isFlashOn = isFlashOn ?? (controller.cameraState.flashMode == .on) + + if isFrontCamera && isFlashOn { + if self.initialBrightness == nil { + self.initialBrightness = UIScreen.main.brightness + self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, 1.0) + self.animateBrightnessChange() + } + } else { + if let initialBrightness = self.initialBrightness { + self.initialBrightness = nil + self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, initialBrightness) + self.animateBrightnessChange() + } + } + } + + private func animateBrightnessChange() { + if self.brightnessAnimator == nil { + self.brightnessAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.animateBrightnessChange() + }) + self.brightnessAnimator?.isPaused = true + } + + if let (startTime, duration, initial, target) = self.brightnessArguments { + self.brightnessAnimator?.isPaused = false + + let t = CGFloat(max(0.0, min(1.0, (CACurrentMediaTime() - startTime) / duration))) + let value = initial + (target - initial) * t + + UIScreen.main.brightness = value + + if t >= 1.0 { + self.brightnessArguments = nil + self.brightnessAnimator?.isPaused = true + self.brightnessAnimator?.invalidate() + self.brightnessAnimator = nil + } + } else { + self.brightnessAnimator?.isPaused = true + self.brightnessAnimator?.invalidate() + self.brightnessAnimator = nil + } } func startVideoRecording(pressing: Bool) { @@ -259,11 +415,11 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { let isFirstRecording = initialDuration.isZero controller.node.resumeCameraCapture() - controller.updatePreviewState({ _ in return nil}, transition: .spring(duration: 0.4)) - controller.node.dismissAllTooltips() controller.updateCameraState({ $0.updatedRecording(pressing ? .holding : .handsFree).updatedDuration(initialDuration) }, transition: .spring(duration: 0.4)) + controller.updatePreviewState({ _ in return nil }, transition: .spring(duration: 0.4)) + controller.node.withReadyCamera(isFirstTime: !controller.node.cameraIsActive) { Queue.mainQueue().after(0.15) { self.resultDisposable.set((camera.startRecording() @@ -289,6 +445,10 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { if initialDuration > 0.0 { controller.onResume() } + + if controller.cameraState.position == .front && controller.cameraState.flashMode == .on { + self.updateScreenBrightness() + } } func stopVideoRecording() { @@ -315,6 +475,12 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { controller.updateCameraState({ $0.updatedRecording(.none) }, transition: .spring(duration: 0.4)) } })) + + if let initialBrightness = self.initialBrightness { + self.initialBrightness = nil + self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, initialBrightness) + self.animateBrightnessChange() + } } func lockVideoRecording() { @@ -324,6 +490,14 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { controller.updateCameraState({ $0.updatedRecording(.handsFree) }, transition: .spring(duration: 0.4)) } + func cancelVideoRecording() { + if let initialBrightness = self.initialBrightness { + self.initialBrightness = nil + self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, initialBrightness) + self.animateBrightnessChange() + } + } + func updateZoom(fraction: CGFloat) { guard let camera = self.getController()?.camera else { return @@ -333,16 +507,20 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, present: self.present, startRecording: self.startRecording, stopRecording: self.stopRecording, completion: self.completion, getController: self.getController) + return State(context: self.context, present: self.present, startRecording: self.startRecording, stopRecording: self.stopRecording, cancelRecording: self.cancelRecording, completion: self.completion, getController: self.getController) } static var body: Body { + let frontFlash = Child(Image.self) let flipButton = Child(CameraButton.self) + let flashButton = Child(CameraButton.self) let viewOnceButton = Child(PlainButtonComponent.self) let recordMoreButton = Child(PlainButtonComponent.self) let muteIcon = Child(ZStack.self) + + let flashAction = ActionSlot() return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value @@ -384,6 +562,25 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { } if !component.isPreviewing { + if case .on = component.cameraState.flashMode, case .front = component.cameraState.position { + let frontFlash = frontFlash.update( + component: Image(image: state.image(.flashImage, theme: environment.theme), tintColor: component.cameraState.flashTint.color), + availableSize: availableSize, + transition: .easeInOut(duration: 0.2) + ) + context.add(frontFlash + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + .scale(1.5 - component.cameraState.flashTintSize * 0.5) + .appear(.default(alpha: true)) + .disappear(ComponentTransition.Disappear({ view, transition, completion in + view.superview?.sendSubviewToBack(view) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + })) + ) + } + let flipButton = flipButton.update( component: CameraButton( content: AnyComponentWithIdentity( @@ -412,6 +609,69 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { .appear(.default(scale: true, alpha: true)) .disappear(.default(scale: true, alpha: true)) ) + + let flashContentComponent: AnyComponentWithIdentity + if "".isEmpty { + let flashIconName: String + switch component.cameraState.flashMode { + case .off: + flashIconName = "roundFlash_off" + case .on: + flashIconName = "roundFlash_on" + default: + flashIconName = "roundFlash_off" + } + + flashContentComponent = AnyComponentWithIdentity( + id: "animatedIcon", + component: AnyComponent( + LottieComponent( + content: LottieComponent.AppBundleContent(name: flashIconName), + color: environment.theme.list.itemAccentColor, + startingPosition: !component.cameraState.flashModeDidChange ? .end : .begin, + size: CGSize(width: 40.0, height: 40.0), + loop: false, + playOnce: flashAction + ) + ) + ) + } else { + flashContentComponent = AnyComponentWithIdentity( + id: "staticIcon", + component: AnyComponent( + Image( + image: state.image(.flash, theme: environment.theme), + tintColor: environment.theme.list.itemAccentColor, + size: CGSize(width: 30.0, height: 30.0) + ) + ) + ) + } + + if !environment.metrics.isTablet { + let flashButton = flashButton.update( + component: CameraButton( + content: flashContentComponent, + minSize: CGSize(width: 44.0, height: 44.0), + isExclusive: false, + action: { [weak state] in + if let state { + state.toggleFlashMode() + Queue.mainQueue().justDispatch { + flashAction.invoke(Void()) + } + } + } + ), + availableSize: availableSize, + transition: context.transition + ) + context.add(flashButton + .position(CGPoint(x: flipButton.size.width + 8.0 + flashButton.size.width / 2.0 + 11.0, y: availableSize.height - flashButton.size.height / 2.0 - 8.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + } } if showViewOnce { @@ -590,6 +850,7 @@ public class VideoMessageCameraScreen: ViewController { fileprivate let startRecording = ActionSlot() fileprivate let stopRecording = ActionSlot() + fileprivate let cancelRecording = ActionSlot() private let completion = ActionSlot() var cameraState: CameraState { @@ -665,6 +926,10 @@ public class VideoMessageCameraScreen: ViewController { self.cameraState = CameraState( position: isFrontPosition ? .front : .back, + flashMode: .off, + flashModeDidChange: false, + flashTint: .white, + flashTintSize: 1.0, recording: .none, duration: 0.0, isDualCameraEnabled: isDualCameraEnabled, @@ -770,12 +1035,15 @@ public class VideoMessageCameraScreen: ViewController { secondaryPreviewView: self.additionalPreviewView ) - self.cameraStateDisposable = (camera.position - |> deliverOnMainQueue).start(next: { [weak self] position in + self.cameraStateDisposable = combineLatest( + queue: Queue.mainQueue(), + camera.flashMode, + camera.position + ).start(next: { [weak self] flashMode, position in guard let self else { return } - self.cameraState = self.cameraState.updatedPosition(position) + self.cameraState = self.cameraState.updatedPosition(position).updatedFlashMode(flashMode) if !self.cameraState.isDualCameraEnabled { self.animatePositionChange() @@ -1131,7 +1399,7 @@ public class VideoMessageCameraScreen: ViewController { } var backgroundFrame = CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: controller.inputPanelFrame.0.minY)) - if backgroundFrame.maxY < layout.size.height - 100.0 && (layout.inputHeight ?? 0.0).isZero && !controller.inputPanelFrame.1 { + if backgroundFrame.maxY < layout.size.height - 100.0 && (layout.inputHeight ?? 0.0).isZero && !controller.inputPanelFrame.1 && layout.additionalInsets.bottom.isZero { backgroundFrame = CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: layout.size.height - layout.intrinsicInsets.bottom - controller.inputPanelFrame.0.height)) } @@ -1221,6 +1489,7 @@ public class VideoMessageCameraScreen: ViewController { }, startRecording: self.startRecording, stopRecording: self.stopRecording, + cancelRecording: self.cancelRecording, completion: self.completion ) ), @@ -1582,7 +1851,7 @@ public class VideoMessageCameraScreen: ViewController { guard let self else { return } - let values = MediaEditorValues(peerId: self.context.account.peerId, originalDimensions: dimensions, cropOffset: .zero, cropRect: CGRect(origin: .zero, size: dimensions.cgSize), cropScale: 1.0, cropRotation: 0.0, cropMirroring: false, cropOrientation: nil, gradientColors: nil, videoTrimRange: self.node.previewState?.trimRange, videoIsMuted: false, videoIsFullHd: false, videoIsMirrored: false, videoVolume: nil, additionalVideoPath: nil, additionalVideoIsDual: false, additionalVideoPosition: nil, additionalVideoScale: nil, additionalVideoRotation: nil, additionalVideoPositionChanges: [], additionalVideoTrimRange: nil, additionalVideoOffset: nil, additionalVideoVolume: nil, nightTheme: false, drawing: nil, maskDrawing: nil, entities: [], toolValues: [:], audioTrack: nil, audioTrackTrimRange: nil, audioTrackOffset: nil, audioTrackVolume: nil, audioTrackSamples: nil, qualityPreset: .videoMessage) + let values = MediaEditorValues(peerId: self.context.account.peerId, originalDimensions: dimensions, cropOffset: .zero, cropRect: CGRect(origin: .zero, size: dimensions.cgSize), cropScale: 1.0, cropRotation: 0.0, cropMirroring: false, cropOrientation: nil, gradientColors: nil, videoTrimRange: self.node.previewState?.trimRange, videoIsMuted: false, videoIsFullHd: false, videoIsMirrored: false, videoVolume: nil, additionalVideoPath: nil, additionalVideoIsDual: false, additionalVideoPosition: nil, additionalVideoScale: nil, additionalVideoRotation: nil, additionalVideoPositionChanges: [], additionalVideoTrimRange: nil, additionalVideoOffset: nil, additionalVideoVolume: nil, nightTheme: false, drawing: nil, maskDrawing: nil, entities: [], toolValues: [:], audioTrack: nil, audioTrackTrimRange: nil, audioTrackOffset: nil, audioTrackVolume: nil, audioTrackSamples: nil, coverImageTimestamp: nil, qualityPreset: .videoMessage) var resourceAdjustments: VideoMediaResourceAdjustments? = nil if let valuesData = try? JSONEncoder().encode(values) { @@ -1622,7 +1891,7 @@ public class VideoMessageCameraScreen: ViewController { context.account.postbox.mediaBox.storeCachedResourceRepresentation(resource, representation: CachedVideoFirstFrameRepresentation(), data: data) } - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: finalDuration, size: video.dimensions, flags: [.instantRoundVideo], preloadSize: nil)]) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: finalDuration, size: video.dimensions, flags: [.instantRoundVideo], preloadSize: nil, coverTime: nil)]) var attributes: [MessageAttribute] = [] if self.cameraState.isViewOnceEnabled { @@ -1683,6 +1952,8 @@ public class VideoMessageCameraScreen: ViewController { } public func discardVideo() { + self.node.cancelRecording.invoke(Void()) + self.requestDismiss(animated: true) } diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/Settings/Browser.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Camera/VideoMessageFlash.imageset/Contents.json similarity index 75% rename from submodules/TelegramUI/Images.xcassets/Instant View/Settings/Browser.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Camera/VideoMessageFlash.imageset/Contents.json index 0fff16d67ed..4b8655a27a4 100644 --- a/submodules/TelegramUI/Images.xcassets/Instant View/Settings/Browser.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Camera/VideoMessageFlash.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "ic_lt_safari.pdf", + "filename" : "flash.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Camera/VideoMessageFlash.imageset/flash.pdf b/submodules/TelegramUI/Images.xcassets/Camera/VideoMessageFlash.imageset/flash.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cc1baf3f944593e2784a1061c22d3c00f5ef0c84 GIT binary patch literal 1549 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f-!aK>2)HOJZ;~) zG}keS*T#vPC8)V&LPee3a=yD9RZl;MKDx1A|M}I|{PuSD?eLE#wjYlE&~o*+{+qa*86S@{S9*sk@3>jiaav31gkjeg z!`%-~N4bf=UcN2W(dNgdwCo=pRdeq1q?kNSW}RUr_An**ZN<|wU%oVMXpi`OPu2Zt zWdG!ge+BYu7Ygmn=svP#v)`#tr?qq^&RGAd@A~~4ZzFu$PCc5-zDN9x^6R8_P3ITRUAkW(HdM#&GPAVe^w3iWEQNZxSM>#kyYt87Ux{^N zb=n^~HELyGrg{9olR0xT4G=vG0|8+##@uGwo_Cy?e(Wj zmAxnUrFbrF>u}4c&|xvTS$Bpb^EapBJuS{{mPr$~o5i1bTxy;5<5Usbx31$dE4Rt} z#|B+Wn*Dy|;iCA)2@%I?0;+Ue0-y1(mh#;Gv^FpyZ*#lF@gteuPSTxX2BLB8hEudA z8wzvz1-TXkF1dWDCud@wPT!}i$CI>0W^eJzwLCR{O6KH;Q7s>xO-@AlSbU#jXLyWn zn$w!C0vdS&JS__o!W}2Q&{Avb6x;Pqb$06J33*}517^iCp43d9EW*^vbI~(Dbj}ll z@U08Za`fHrS1D^d{Zex2)_~5dP1nvi`KtML%Dvf?z28>wm-2_W7Jm=xg$A3QROJFb zg!gU_RC^qe(G_yNVa9>2)hi`uhBY%v)|82MY&o$^z$#>N@Pl139x5|mzSc4=sggM1 z@ZLA`+Q&VDf2Hm)d@N-T`s}#w`qk`^hROfcTKJM;FP7)je-nAi&YE48?X-}iVfC7s zSvs?tU##N|?SB2({`mFBZ$AoGA55_{MkX*J1Pf+TP-=00X;E@& zu>z=&0hJD*0>e2!uOu}OXd|eEfe8jA76HW+Orb(Z0188ap@9OJrH}^~G6uR61Q0@&mX>Hjh6YB!&_PjUXlRM9(a^}u0hA^s6`xo` literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/Contents.json new file mode 100644 index 00000000000..6e7b34e21d4 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "privacy (2).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/privacy (2).pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/privacy (2).pdf new file mode 100644 index 0000000000000000000000000000000000000000..badb2fabc505b01ea105299eda4038ada4089b0d GIT binary patch literal 2500 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Bookmark.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Bookmark.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d413418c5dd4d01c2aab27b759af9edee4605a6f GIT binary patch literal 2667 zcmZXWc|26>8^BE=CcBa@=~yO1W|*;WlR?NjmTqO~8q8%fW->F#R^tw3jcUdgvZNyX zY%N5Q8N$62MV7+N9%XKkC0FiDi@W@uKhEbp-_QHJ@AEw8kMo6KtZiTjxFHyf0CWJ7 z-zhKvn3(}66c|Sg;J<>A|K!>>cmfqi2DEJmSSrpM=T8d2@iKU!?T8}=QiA|p-gY-0 z;+;UDl5yA&@EK5SmeWNAL#c!nlLUB%LCPa`hzu~VW-Fy=)RG$4V!`2ZT|FXPB3_=~ zldE2P_T?#FwY|<`yTsU8+CFZVpN}W`aOaUVR|BS66mfi)P4Jt%ruK^wxEi!CSpOI2LtNj2 zB1l~2`@E-b?slG~rKK4pyYHcVK1Ca{ax7=DJFx(~H5l&TwA+ciI7#!!Q<) zGBG7EN?Vc~4fB@d>4eEr!-F8DVZUaG`)%)hW?Ex_NkKt}o*Da@PHz@?3FJ}dr1PwP zFJ`E6K)ZG0qcqVT_d7nRAYOJMdAgMC7E^svL#VKEW813*dezDbbX0|Y;>lt`W?xj| zMP2eAm|H2sDg&AlRl@VOm8a{Wwlg=*J%T_vMg0Rue4euQdcm}>lq(BqYQ7s?87gS9 z3$@8Ss4T-sYDONjcrxbF`|%-bCN;Jpabv@VyC)qXYigBIs0tstzi$m=$+@$r+^6KY z-*4w{(kECodv8@sJvhIkq2fh{hS>$@Y?pG*{Hf@0%MaGiC@c&5JR^dH ziw~4@v`MO>tfEm%Cc4`*U(263V%?9lOg%7}{If+%jX`6?B>Jtl)Y`-C&!5R&PlpL! zmuNd=F8iETeCgmxvv|uUNgo(gL3zT&lde+uR^v`!3~WjzC`jRXcZAhTB?kA@wfns& z@}=wuHB~J;=Q#VpIS@=vCR=`F-tUH(S8)BAWn$j891S78`yo$q*F-i&k|A@p1&qtZ z=D}Ts+BLwOXIMV7WA{6+bVB9*?9wNvfIqG5+t`SRsUwEVyLo+QL@6wwk~ff0YTpxiV>lKOl*S?Frh2aXjfyN|h^jASIsz0+i+#9Ka| zt0-}1TBpo1LBZwv2P9Q01_wb{V7k!*bMaO4W%O|Ef^o-IOX2Vi~Xjv}Zg1t5|f`>Bk2O10PB!h_FQ%c!(g&~N8yEL^*^Vbzb zuy`kj)9^?cEBI@f&+QUaAM-lo6O#e?S}8hFy!R)FQsT(Grq&ojKy}uid>>@iW6^HZ znRZ$7kFpP+a5R}Dk74)Qv>#Ibj19!5-@dTA%i!`3iu|k5^7!A99fgmYGU}ys;_bvL zir$QOc!EF0zkMFnlc+vi6g_iCt*`GX-8(+fgns0YC73<{u%rm6<%WE0a_Cfmt=EGc z=DWgP(4pf+;#WLpS6iJ9V0GqP%LI>43ek*2<||&RU1cp4EoOqIayUjKO#j)w0KXuO zb;9Kuw7-S`NapFJB@2z*-2+G~lt2`x4H$+;ZOQnGxU@vqo!8xCXf^A-Xq~Pf%XWq$ z=cpyt@!Y)^SSwIV%jfUYe?LZ$ND*^Fe`+-YZdqZ1?so4-oi9vu$wXVhu?0p!0-pO) z%{ou{a>UYIPOg?2=K6Heh_Z;Wj^jh(F}bJWItV$3mIu3p?{)gs))doC%OfkyUb#lr z(~pX12B#{>!xdr=J2x@SE{z;6Xd6hna>@Lt%{|-|SkG?dl{bgOwRb1Dd=wcho3D

L zniHuz(P}q#w5j>hm#VS>7UtG*ifC^CzG+~RyiL>SSLv%8V) zbWFNk_UapKddHBGS0o0hj>+?3}F_9*R(aS#oRn3!Rx=EvNizm~_@ zJF2VC5L$I7w^gd1>BZJYfH9$)l=Z0f#pQj^#l>{^UhwV5{@oDxzx=P}fG{>T+7bu7 zEzA=NZ8VWcqEY}apKrS05qTO1Xk)N}-~GV}7l6lFlFk5LfDT;eOZ>gi-9mq_zMtfV zqmaVL{x}N2R~Wu~@O8wBM5N+~yc_xc@*QHZWF7X&wqdV2a`2;{Jf F&0n1gYytoP literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Contents.json new file mode 100644 index 00000000000..09b651c240a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Bookmark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Browser.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Browser.pdf new file mode 100644 index 0000000000000000000000000000000000000000..81f7ac317102df77bcaf0b495208ee9ccf7db77b GIT binary patch literal 2160 zcmZXVdpMNo8^@8unEaLua>~A}^P!nx4C6PLF^tnxV+c8A@RHNanwd0{a%gCsjo7Ib z5!oc=5Zkhably%vE4EaFkknu)Np^;4+r9sIujlz*&wW4l_kOSI`52I$i5AGMHc%)M zKmhcx7$^W>v4DdE6r@Fnw@}M}>TqIo91COsaAF*l1v-P_^axOtAqqGNq(!mz09K;o z6pj}tv*jJjGwhkKX$XaAgjMvaO(BAXR+2dLk zqaWGrPmNMg?e-ly{QE=4KmCs1Gd!Kxe4%=$?yq<*EN6JC6w~r(2#-J1U$lr;*9PM^ zG`)|Q94+XTNkFh~Ucv{C^>nmifZtATQ$9Mhr@^?b;d#$ZE=CsfmI+QMWnz!CtPWS*V%lqCO~D z2cr=}R4XfCHWy`Be3U&BaM|$4xY2iL<0}=upnnR>wr6}}_#eJ}9?CN<_vNDjkA(BZ zktJQ(=X#8Y9xaBgFyFTuxE}MqNJa^Bz`-wIYkpzKoVqbvchhfE27c-+{#5rHkSTZQ#ueGh zCUCUMsYlmP|CJ}!YbH#(agq1`iU6*p^F_PteA(?VXFoZae4^qbRq|6;&s4G!gokn( z2uAw^^O}9Ecq18gOEsvQwucok66uzU9yn#Q!)EG^9QVHBf~xa#D&^J7JBx`V8H3I+ z3S{2eUZe54vnpd2<4p^_t0OIli-^19)?9X*a+H_RV))`}QPbcdmo$hoAJo?^3+0;H znco2m^)@B+I?Xz#gD~Aobz9_iPpI(k119Fv-&ZlS%VopT)TCdQW}o`NsU(0+Dhbz4 zneT}mnNO&HeULl<*s=WyNsxQcs>c!~Klm`Bj`8O7mM-GfZM!+jaU^^B(m#|FR4Yib z0jg7(r~C!Yfshb2yI$q49*v)qW`P5a7Kwbt8d~XynwK#5^)18JgcA17)>~=pkE~s- zfD(D4CLF2iCVSca+Y%i?{8(jto0j{-5Y*KP+)qE`Q3&j7@x9@{ld`wsxnT@vao|l_2t#A^=>c!vK*Vz5E z;C7_F)I*nEwp!L6(iA>XCpN%U)y(cyYEa=Zp9%-pT3Dvy3Hzcv-^Y_&bIm&z`OrE6 zEVJ#FUQs};u}s5}dNqFK*z)s>c#WLRrU>aQV2c7=6cb0|dT=qh+ifAF!)d@`AnYUB;SVHYJ{q%qSR`FI&^*wQ!)ko%m=k$E?eI5kR?bC_6bgtn3$iuA)07X zfwLy_Zu0?`@-GmD1QhuP)N1dL@_JlEX0%M zlv?#VIw6v+l6c*AEoJ$Tw)aw8uEf<;bB7nBpOPnJ&OlO@SqJPt*|u*W$dp3FaQOVX zuVF&`?yuqliNYY)>4938MO1;~X*4>E2?PdzW`+;vdk}z=sZnbX*#<^LQ=RB+AP_)o zMXbb^h1I(BWwlm`0y61)8Q~xk5OYinATg5&bQ%k!iEb1_Y)vFn86pj^{w)0ibz*59 zfpE|02vMtm*Z~DtyN5vEE6PRwr+?-u^46w-{;HDLAangfabv5-2~-v}jvn=ODw_dD zLJ@!sbk)B23!ts7t*il&{5g;aU}Xi+)+H-ZpW;}TkZ1(vKa!Pb5MQ6NMTlJgOS1Wr z$YM~V<3I*f{P*0QMdL7L(&IrBz&JX>WOXQT3Z2dZ#D-S}4EK)S4~n{9;XjqZTCsz~ NSlL1i3|w{){|5EXWUT-I literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Contents.json new file mode 100644 index 00000000000..c45b00a1dea --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Browser.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Close.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Close.pdf new file mode 100644 index 0000000000000000000000000000000000000000..dc18df012da3d8f599fc17d5e8e3935ac262b84b GIT binary patch literal 1428 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/Settings/Browser.imageset/ic_lt_safari.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/Settings/Browser.imageset/ic_lt_safari.pdf deleted file mode 100644 index ba73353a6d6e002a2b8e4e3cfee270797252fdc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4345 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 GeElD%%slJ> literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Gift.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Gift.imageset/Contents.json new file mode 100644 index 00000000000..638f9f66184 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Gift.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "giftstars_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Gift.imageset/giftstars_24.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Gift.imageset/giftstars_24.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ed459de9e719f38f495e6661c366a535a4e4fb40 GIT binary patch literal 9544 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Stories/Context Menu/EditCover.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/Context Menu/EditCover.imageset/Contents.json new file mode 100644 index 00000000000..b6215a7a9da --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/Context Menu/EditCover.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "storycover_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Stories/Context Menu/EditCover.imageset/storycover_24.pdf b/submodules/TelegramUI/Images.xcassets/Stories/Context Menu/EditCover.imageset/storycover_24.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cef5ed510f48da9496feae5a6ff82a121de451a9 GIT binary patch literal 2441 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/StarsSend.tgs b/submodules/TelegramUI/Resources/Animations/StarsSend.tgs new file mode 100644 index 0000000000000000000000000000000000000000..3d4e32a67eb427151adbffaa08b57e2b6be52462 GIT binary patch literal 5796 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 i_}2cVFK_?S7ZqLp8w}5YeReMl{r>>s6Iw=0; i--) { uiWebview_HighlightAllOccurencesOfStringForElement(element.childNodes[i],keyword); } @@ -68,6 +102,7 @@ function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) { // the main entry point to start the search function uiWebview_HighlightAllOccurencesOfString(keyword) { uiWebview_RemoveAllHighlights(); + uiWebview_AddDarkOverlay(); uiWebview_HighlightAllOccurencesOfStringForElement(document.body, keyword.toLowerCase()); } @@ -100,9 +135,24 @@ function uiWebview_RemoveAllHighlightsForElement(element) { function uiWebview_RemoveAllHighlights() { uiWebview_SearchResultCount = 0; uiWebview_RemoveAllHighlightsForElement(document.body); + uiWebview_RemoveDarkOverlay(); } function uiWebview_ScrollTo(idx) { var scrollTo = document.getElementById("SEARCH WORD" + idx); if (scrollTo) scrollTo.scrollIntoView(); } + +function uiWebview_AddDarkOverlay() { + var overlay = document.createElement('div'); + overlay.classList.add('dark-overlay'); + overlay.setAttribute('id', 'dark-overlay'); + document.body.appendChild(overlay); +} + +function uiWebview_RemoveDarkOverlay() { + var overlay = document.getElementById('dark-overlay'); + if (overlay) { + document.body.removeChild(overlay); + } +} diff --git a/submodules/TelegramUI/Resources/currencies.json b/submodules/TelegramUI/Resources/currencies.json index a4ffa9567d2..2b2c35ccc0e 100644 --- a/submodules/TelegramUI/Resources/currencies.json +++ b/submodules/TelegramUI/Resources/currencies.json @@ -1447,5 +1447,14 @@ "symbolOnLeft": true, "spaceBetweenAmountAndSymbol": false, "decimalDigits": 2 - } + }, + "BYN": { + "code": "BYN", + "symbol": "BYN", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + } } diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 99b8c572da5..7b76b902c0d 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -201,7 +201,7 @@ final class SharedApplicationContext { let notificationManager: SharedNotificationManager let wakeupManager: SharedWakeupManager let overlayMediaController: ViewController & OverlayMediaController - var minimizedContainer: MinimizedContainer? + var minimizedContainer: [AccountRecordId: MinimizedContainer] = [:] init(sharedContext: SharedAccountContextImpl, notificationManager: SharedNotificationManager, wakeupManager: SharedWakeupManager) { self.sharedContext = sharedContext diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index bb989c7cc0e..245323180c6 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -31,6 +31,8 @@ import StoryContainerScreen import ChatMessageNotificationItem import PhoneNumberFormat import AttachmentUI +import MinimizedContainer +import BrowserUI final class UnauthorizedApplicationContext { let sharedContext: SharedAccountContextImpl @@ -169,9 +171,12 @@ final class AuthorizedApplicationContext { self.notificationController = NotificationContainerController(context: context) self.rootController = TelegramRootController(context: context) - self.rootController.minimizedContainer = self.sharedApplicationContext.minimizedContainer + self.rootController.minimizedContainer = self.sharedApplicationContext.minimizedContainer[context.account.id] self.rootController.minimizedContainerUpdated = { [weak self] minimizedContainer in - self?.sharedApplicationContext.minimizedContainer = minimizedContainer + guard let self else { + return + } + self.sharedApplicationContext.minimizedContainer[self.context.account.id] = minimizedContainer } self.rootController.globalOverlayControllersUpdated = { [weak self] in @@ -437,8 +442,12 @@ final class AuthorizedApplicationContext { return false } - if let topContoller = strongSelf.rootController.topViewController as? AttachmentController { + if let minimizedContainer = strongSelf.rootController.minimizedContainer, minimizedContainer.isExpanded { + minimizedContainer.collapse() + } else if let topContoller = strongSelf.rootController.topViewController as? AttachmentController { topContoller.minimizeIfNeeded() + } else if let topContoller = strongSelf.rootController.topViewController as? BrowserScreen { + topContoller.requestMinimize(topEdgeOffset: nil, initialVelocity: nil) } for controller in strongSelf.rootController.viewControllers { @@ -817,7 +826,7 @@ final class AuthorizedApplicationContext { return } - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: strongSelf.rootController, context: strongSelf.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil))) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: strongSelf.rootController, context: strongSelf.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) }) } @@ -936,7 +945,7 @@ final class AuthorizedApplicationContext { chatLocation = .peer(peer) } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) } : nil, activateInput: activateInput ? .text : nil)) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil)) }) } } diff --git a/submodules/TelegramUI/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Sources/AttachmentFileController.swift index 4ca7746f485..3a4d0bdd013 100644 --- a/submodules/TelegramUI/Sources/AttachmentFileController.swift +++ b/submodules/TelegramUI/Sources/AttachmentFileController.swift @@ -164,44 +164,6 @@ private func attachmentFileControllerEntries(presentationData: PresentationData, } private final class AttachmentFileContext: AttachmentMediaPickerContext { - var selectionCount: Signal { - return .single(0) - } - - var caption: Signal { - return .single(nil) - } - - var hasCaption: Bool { - return false - } - - var captionIsAboveMedia: Signal { - return .single(false) - } - - func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { - } - - public var loadingProgress: Signal { - return .single(nil) - } - - public var mainButtonState: Signal { - return .single(nil) - } - - func setCaption(_ caption: NSAttributedString) { - } - - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { - } - - func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { - } - - func mainButtonAction() { - } } class AttachmentFileControllerImpl: ItemListController, AttachmentFileController, AttachmentContainable { @@ -215,6 +177,7 @@ class AttachmentFileControllerImpl: ItemListController, AttachmentFileController public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } + public var isMinimized: Bool = false var delayDisappear = false diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 3a87493be9d..53c7603653d 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1023,7 +1023,7 @@ extension ChatControllerImpl { self.chatDisplayNode.historyNode.scrolledToIndex = { [weak self] toSubject, initial in if let strongSelf = self, case let .message(index) = toSubject.index { - if case let .message(messageSubject, _, _) = strongSelf.subject, initial, case let .id(messageId) = messageSubject, messageId != index.id { + if case let .message(messageSubject, _, _, _) = strongSelf.subject, initial, case let .id(messageId) = messageSubject, messageId != index.id { if messageId.peerId == index.id.peerId { strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_MessageDoesntExist, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current) } @@ -1036,6 +1036,12 @@ extension ChatControllerImpl { } if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(mappedId) { + if toSubject.setupReply { + Queue.mainQueue().after(0.1) { + strongSelf.interfaceInteraction?.setupReplyMessage(mappedId, { _, f in f() }) + } + } + let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId, quote: toSubject.quote.flatMap { quote in ChatInterfaceHighlightedState.Quote(string: quote.string, offset: quote.offset) }) controllerInteraction.highlightedState = highlightedState strongSelf.updateItemNodesHighlightedStates(animated: initial) @@ -1066,7 +1072,7 @@ extension ChatControllerImpl { let _ = strongSelf.controllerInteraction?.openMessage(message, OpenMessageParams(mode: .timecode(timecode))) } } - } else if case let .message(_, _, maybeTimecode) = strongSelf.subject, let timecode = maybeTimecode, initial { + } else if case let .message(_, _, maybeTimecode, _) = strongSelf.subject, let timecode = maybeTimecode, initial { Queue.mainQueue().after(0.2) { let _ = strongSelf.controllerInteraction?.openMessage(message, OpenMessageParams(mode: .timecode(timecode))) } @@ -3938,7 +3944,7 @@ extension ChatControllerImpl { } if let navigationController = strongSelf.effectiveNavigationController { - let subject: ChatControllerSubject? = sourceMessageId.flatMap { ChatControllerSubject.message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) } + let subject: ChatControllerSubject? = sourceMessageId.flatMap { ChatControllerSubject.message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(replyThreadResult), subject: subject, keepStack: .always)) } }, activatePinnedListPreview: { [weak self] node, gesture in diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift index 41fe42586fa..eba9090dd9f 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift @@ -29,7 +29,7 @@ extension ChatControllerImpl { guard let self else { return } - self.navigateToMessage(from: fromId, to: .id(id, params), forceInCurrentChat: fromId.peerId == id.peerId && !params.forceNew, forceNew: params.forceNew) + self.navigateToMessage(from: fromId, to: .id(id, params), forceInCurrentChat: fromId.peerId == id.peerId && !params.forceNew, forceNew: params.forceNew, progress: params.progress) } let _ = (self.context.engine.data.get( @@ -77,6 +77,7 @@ extension ChatControllerImpl { animated: Bool = true, completion: (() -> Void)? = nil, customPresentProgress: ((ViewController, Any?) -> Void)? = nil, + progress: Promise? = nil, statusSubject: ChatLoadingMessageSubject = .generic ) { if !self.isNodeLoaded { @@ -147,7 +148,7 @@ extension ChatControllerImpl { } else { navigateToLocation = .peer(peer) } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: navigateToLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always)) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: navigateToLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always)) completion?() }) @@ -160,31 +161,156 @@ extension ChatControllerImpl { guard let self, let peer = peer else { return } - if let navigationController = self.effectiveNavigationController { - var chatLocation: NavigateToChatControllerParams.Location = .peer(peer) - var displayMessageNotFoundToast = false - if case let .channel(channel) = peer, channel.flags.contains(.isForum) { - if let message = message, let threadId = message.threadId { - chatLocation = .replyThread(ChatReplyThreadMessage(peerId: peer.id, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + + var quote: ChatControllerSubject.MessageHighlight.Quote? + if case let .id(_, params) = messageLocation { + quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } + } + var progressValue: Promise? + if let value = progress { + progressValue = value + } else if case let .id(_, params) = messageLocation { + progressValue = params.progress + } + self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) + + var chatLocation: NavigateToChatControllerParams.Location = .peer(peer) + var preloadChatLocation: ChatLocation = .peer(id: peer.id) + var displayMessageNotFoundToast = false + if case let .channel(channel) = peer, channel.flags.contains(.isForum) { + if let message = message, let threadId = message.threadId { + let replyThreadMessage = ChatReplyThreadMessage(peerId: peer.id, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false) + chatLocation = .replyThread(replyThreadMessage) + preloadChatLocation = .replyThread(message: replyThreadMessage) + } else { + displayMessageNotFoundToast = true + } + } + + let searchLocation: ChatHistoryInitialSearchLocation + switch messageLocation { + case let .id(id, _): + if case let .replyThread(message) = chatLocation, id == message.effectiveMessageId { + searchLocation = .index(.absoluteLowerBound()) + } else { + searchLocation = .id(id) + } + case let .index(index): + searchLocation = .index(index) + case .upperBound: + searchLocation = .index(MessageIndex.upperBound(peerId: chatLocation.peerId)) + } + var historyView: Signal + + let subject: ChatControllerSubject = .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil, setupReply: false) + + historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true, setupReply: false), id: 0), context: self.context, chatLocation: preloadChatLocation, subject: subject, chatLocationContextHolder: Atomic(value: nil), fixedCombinedReadStates: nil, tag: nil, additionalData: []) + + var signal: Signal<(MessageIndex?, Bool), NoError> + signal = historyView + |> mapToSignal { historyView -> Signal<(MessageIndex?, Bool), NoError> in + switch historyView { + case .Loading: + return .single((nil, true)) + case let .HistoryView(view, _, _, _, _, _, _): + for entry in view.entries { + if entry.message.id == messageLocation.messageId { + return .single((entry.message.index, false)) + } + } + if case let .index(index) = searchLocation { + return .single((index, false)) + } + return .single((nil, false)) + } + } + |> take(until: { index in + return SignalTakeAction(passthrough: true, complete: !index.1) + }) + + /*#if DEBUG + signal = .single((nil, true)) |> then(signal |> delay(2.0, queue: .mainQueue())) + #endif*/ + + var cancelImpl: (() -> Void)? + let presentationData = self.presentationData + let displayTime = CACurrentMediaTime() + let progressSignal = Signal { [weak self] subscriber in + if let progressValue { + progressValue.set(.single(true)) + return ActionDisposable { + Queue.mainQueue().async() { + progressValue.set(.single(false)) + } + } + } else if case .generic = statusSubject { + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + if CACurrentMediaTime() - displayTime > 1.5 { + cancelImpl?() + } + })) + if let customPresentProgress = customPresentProgress { + customPresentProgress(controller, nil) } else { - displayMessageNotFoundToast = true + self?.present(controller, in: .window(.root)) + } + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } } + } else { + return EmptyDisposable } - - var quote: ChatControllerSubject.MessageHighlight.Quote? - if case let .id(_, params) = messageLocation { - quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } + } + |> runOn(Queue.mainQueue()) + |> delay(progressValue == nil ? 0.05 : 0.0, queue: Queue.mainQueue()) + let progressDisposable = MetaDisposable() + var progressStarted = false + self.messageIndexDisposable.set((signal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + |> deliverOnMainQueue).startStrict(next: { [weak self] index in + guard let self else { + return } - let context = self.context - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil), keepStack: .always, chatListCompletion: { chatListController in - if displayMessageNotFoundToast { - let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_MessageDoesntExist, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in - return true - }), in: .current) + if let index = index.0 { + let _ = index + //strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, quote: quote, scrollPosition: scrollPosition) + } else if index.1 { + if !progressStarted { + progressStarted = true + progressDisposable.set(progressSignal.start()) } - })) + return + } + + if let navigationController = self.effectiveNavigationController { + let context = self.context + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: subject, keepStack: .always, chatListCompletion: { chatListController in + if displayMessageNotFoundToast { + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_MessageDoesntExist, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in + return true + }), in: .current) + } + })) + } + }, completed: { [weak self] in + if let strongSelf = self { + strongSelf.loadingMessage.set(.single(nil)) + } + completion?() + })) + cancelImpl = { [weak self] in + if let strongSelf = self { + strongSelf.loadingMessage.set(.single(nil)) + strongSelf.messageIndexDisposable.set(nil) + } } completion?() @@ -214,11 +340,13 @@ extension ChatControllerImpl { } var quote: (string: String, offset: Int?)? + var setupReply = false if case let .id(_, params) = messageLocation { quote = params.quote.flatMap { quote in (string: quote.string, offset: quote.offset) } + setupReply = params.setupReply } - - self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, scrollPosition: scrollPosition) + + self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, scrollPosition: scrollPosition, setupReply: setupReply) if delayCompletion { Queue.mainQueue().after(0.25, { @@ -236,12 +364,14 @@ extension ChatControllerImpl { } else if case let .index(index) = messageLocation, index.id.id == 0, index.timestamp > 0, case .scheduledMessages = self.presentationInterfaceState.subject { self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, scrollPosition: scrollPosition) } else { + var setupReply = false var quote: (string: String, offset: Int?)? if case let .id(messageId, params) = messageLocation { if params.timestamp != nil { self.scheduledScrollToMessageId = (messageId, params) } quote = params.quote.flatMap { ($0.string, $0.offset) } + setupReply = params.setupReply } var progress: Promise? if case let .id(_, params) = messageLocation { @@ -267,7 +397,7 @@ extension ChatControllerImpl { } } var historyView: Signal - historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: []) + historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true, setupReply: setupReply), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: []) var signal: Signal<(MessageIndex?, Bool), NoError> signal = historyView @@ -338,7 +468,7 @@ extension ChatControllerImpl { } |> deliverOnMainQueue).startStrict(next: { [weak self] index in if let strongSelf = self, let index = index.0 { - strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, quote: quote, scrollPosition: scrollPosition) + strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, quote: quote, scrollPosition: scrollPosition, setupReply: setupReply) } else if index.1 { if !progressStarted { progressStarted = true @@ -380,11 +510,13 @@ extension ChatControllerImpl { self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) var quote: ChatControllerSubject.MessageHighlight.Quote? + var setupReply = false if case let .id(_, params) = messageLocation { quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } + setupReply = params.setupReply } - let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: quote.flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: []) + let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: quote.flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: 50, highlight: true, setupReply: setupReply), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: []) var signal: Signal signal = historyView |> mapToSignal { historyView -> Signal in @@ -416,11 +548,13 @@ extension ChatControllerImpl { if let navigationController = strongSelf.effectiveNavigationController { var quote: ChatControllerSubject.MessageHighlight.Quote? + var setupReply = false if case let .id(_, params) = messageLocation { quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } + setupReply = params.setupReply } - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil) }, keepStack: .always)) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil, setupReply: setupReply) }, keepStack: .always)) } }) completion?() @@ -438,7 +572,7 @@ extension ChatControllerImpl { return } if let navigationController = self.effectiveNavigationController { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) })) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) })) } completion?() }) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift index c3d6c7070ba..d44e93258bc 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift @@ -183,7 +183,7 @@ extension ChatControllerImpl { if canAddToReadingList { items.append( - .action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_AddToReadingList, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { _, f in + .action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_AddToReadingList, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReadingList"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) if let link = URL(string: url) { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift index 77353f1fad7..20b2ba01a12 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift @@ -16,113 +16,16 @@ import TextFormat import TelegramBaseController import AccountContext import TelegramStringFormatting -import OverlayStatusController -import DeviceLocationManager -import ShareController -import UrlEscaping -import ContextUI -import ComposePollUI -import AlertUI import PresentationDataUtils import UndoUI -import TelegramCallsUI -import TelegramNotices -import GameUI -import ScreenCaptureDetection -import GalleryUI -import OpenInExternalAppUI -import LegacyUI -import InstantPageUI -import LocationUI -import BotPaymentsUI -import DeleteChatPeerActionSheetItem -import HashtagSearchUI -import LegacyMediaPickerUI -import Emoji -import PeerAvatarGalleryUI import PeerInfoUI -import RaiseToListen -import UrlHandling -import AvatarNode import AppBundle import LocalizedPeerData -import PhoneNumberFormat -import SettingsUI -import UrlWhitelist -import TelegramIntents -import TooltipUI -import StatisticsUI -import MediaResources -import GalleryData import ChatInterfaceState -import InviteLinksUI -import Markdown -import TelegramPermissionsUI -import Speak -import TranslateUI -import UniversalMediaPlayer -import WallpaperBackgroundNode -import ChatListUI -import CalendarMessageScreen -import ReactionSelectionNode -import ReactionListContextMenuContent -import AttachmentUI -import AttachmentTextInputPanelNode -import MediaPickerUI -import ChatPresentationInterfaceState -import Pasteboard -import ChatSendMessageActionUI -import ChatTextLinkEditUI -import WebUI -import PremiumUI -import ImageTransparency -import StickerPackPreviewUI -import TextNodeWithEntities -import EntityKeyboard -import ChatTitleView -import EmojiStatusComponent -import ChatTimerScreen -import MediaPasteboardUI -import ChatListHeaderComponent import ChatControllerInteraction -import FeaturedStickersScreen -import ChatEntityKeyboardInputNode -import StorageUsageScreen -import AvatarEditorScreen -import ChatScheduleTimeController -import ICloudResources import StoryContainerScreen -import MoreHeaderButton -import VolumeButtons -import ChatAvatarNavigationNode -import ChatContextQuery -import PeerReportScreen -import PeerSelectionController import SaveToCameraRoll -import ChatMessageDateAndStatusNode -import ReplyAccessoryPanelNode -import TextSelectionNode -import ChatMessagePollBubbleContentNode -import ChatMessageItem -import ChatMessageItemImpl -import ChatMessageItemView -import ChatMessageItemCommon -import ChatMessageAnimatedStickerItemNode -import ChatMessageBubbleItemNode -import ChatNavigationButton -import WebsiteType -import ChatQrCodeScreen -import PeerInfoScreen import MediaEditorScreen -import WallpaperGalleryScreen -import WallpaperGridScreen -import VideoMessageCameraScreen -import TopMessageReactions -import AudioWaveform -import PeerNameColorScreen -import ChatEmptyNode -import ChatMediaInputStickerGridItem -import AdsInfoScreen extension ChatControllerImpl { func openStorySharing(messages: [Message]) { @@ -187,7 +90,6 @@ extension ChatControllerImpl { } } }) - } ) self.push(controller) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index 579cc0fe4f2..5d89917c5cf 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -12,33 +12,39 @@ import TelegramNotices import PresentationDataUtils import UndoUI import UrlHandling +import TelegramPresentationData -public extension ChatControllerImpl { - func openWebApp(buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource) { - guard let peerId = self.chatLocation.peerId, let peer = self.presentationInterfaceState.renderedPeer?.peer else { - return - } - self.chatDisplayNode.dismissInput() - - let botName: String - let botAddress: String - if case let .inline(bot) = source { - botName = bot.compactDisplayTitle - botAddress = bot.addressName ?? "" - } else { - botName = EnginePeer(peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) - botAddress = peer.addressName ?? "" - } - - if source == .generic { - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { +func openWebAppImpl(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) { + let presentationData: PresentationData + if let parentController = parentController as? ChatControllerImpl { + presentationData = parentController.presentationData + } else { + presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + } + + let botName: String + let botAddress: String + let botVerified: Bool + if case let .inline(bot) = source { + botName = bot.compactDisplayTitle + botAddress = bot.addressName ?? "" + botVerified = bot.isVerified + } else { + botName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + botAddress = peer.addressName ?? "" + botVerified = peer.isVerified + } + + if source == .generic { + if let parentController = parentController as? ChatControllerImpl { + parentController.updateChatPresentationInterfaceState(animated: true, interactive: true, { return $0.updatedTitlePanelContext { if !$0.contains(where: { switch $0 { - case .requestInProgress: - return true - default: - return false + case .requestInProgress: + return true + default: + return false } }) { var updatedContexts = $0 @@ -49,364 +55,507 @@ public extension ChatControllerImpl { } }) } - - let updateProgress = { [weak self] in - Queue.mainQueue().async { - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedTitlePanelContext { - if let index = $0.firstIndex(where: { - switch $0 { - case .requestInProgress: - return true - default: - return false - } - }) { - var updatedContexts = $0 - updatedContexts.remove(at: index) - return updatedContexts + } + + let updateProgress = { [weak parentController] in + Queue.mainQueue().async { + if let parentController = parentController as? ChatControllerImpl { + parentController.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if let index = $0.firstIndex(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false } - return $0 + }) { + var updatedContexts = $0 + updatedContexts.remove(at: index) + return updatedContexts } - }) - } + return $0 + } + }) } } - - let openWebView = { - if source == .menu { - self.updateChatPresentationInterfaceState(interactive: false) { state in + } + + + let openWebView = { [weak parentController] in + guard let parentController else { + return + } + if source == .menu { + if let parentController = parentController as? ChatControllerImpl { + parentController.updateChatPresentationInterfaceState(interactive: false) { state in return state.updatedForceInputCommandsHidden(true) -// return state.updatedShowWebView(true).updatedForceInputCommandsHidden(true) } - - if let navigationController = self.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer { - for controller in minimizedContainer.controllers { - if let controller = controller as? AttachmentController, let mainController = controller.mainController as? WebAppController, mainController.botId == peerId && mainController.source == .menu { - navigationController.maximizeViewController(controller, animated: true) - return - } + } + + if let navigationController = parentController.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer { + for controller in minimizedContainer.controllers { + if let controller = controller as? AttachmentController, let mainController = controller.mainController as? WebAppController, mainController.botId == peer.id && mainController.source == .menu { + navigationController.maximizeViewController(controller, animated: true) + return } } - - var fullSize = false - if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: self.context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl { - fullSize = !url.contains("?mode=compact") + } + + var fullSize = false + if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl { + fullSize = !url.contains("?mode=compact") + } + + var presentImpl: ((ViewController, Any?) -> Void)? + let params = WebAppParameters(source: .menu, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: 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) + }, commit: commit) + }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in + ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: peer.id, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) + }, getInputContainerNode: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl, let layout = parentController.validLayout, case .compact = layout.metrics.widthClass { + return (parentController.chatDisplayNode.getWindowInputAccessoryHeight(), parentController.chatDisplayNode.inputPanelContainerNode, { + return parentController.chatDisplayNode.textInputPanelNode?.makeAttachmentMenuTransition(accessoryPanelNode: nil) + }) + } else { + return nil } - - let context = self.context - let params = WebAppParameters(source: .menu, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize) - let controller = standaloneWebAppController(context: self.context, updatedPresentationData: self.updatedPresentationData, params: params, threadId: self.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in - self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) - }, requestSwitchInline: { [weak self] query, chatTypes, completion in - if let strongSelf = self { - if let chatTypes { - let controller = strongSelf.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: strongSelf.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false)) - controller.peerSelected = { [weak self, weak controller] peer, _ in - if let strongSelf = self { - completion() - controller?.dismiss() - strongSelf.controllerInteraction?.activateSwitchInline(peer.id, "@\(botAddress) \(query)", nil) - } - } - strongSelf.push(controller) - } else { - strongSelf.controllerInteraction?.activateSwitchInline(peerId, "@\(botAddress) \(query)", nil) - } - } - }, getInputContainerNode: { [weak self] in - if let strongSelf = self, let layout = strongSelf.validLayout, case .compact = layout.metrics.widthClass { - return (strongSelf.chatDisplayNode.getWindowInputAccessoryHeight(), strongSelf.chatDisplayNode.inputPanelContainerNode, { - return strongSelf.chatDisplayNode.textInputPanelNode?.makeAttachmentMenuTransition(accessoryPanelNode: nil) - }) - } else { - return nil - } - }, completion: { [weak self] in - self?.chatDisplayNode.historyNode.scrollToEndOfHistory() - }, willDismiss: { [weak self] in - self?.interfaceInteraction?.updateShowWebView { _ in + }, completion: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl { + parentController.chatDisplayNode.historyNode.scrollToEndOfHistory() + } + }, willDismiss: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl { + parentController.interfaceInteraction?.updateShowWebView { _ in return false } - }, didDismiss: { [weak self] in - if let strongSelf = self { - let isFocused = strongSelf.chatDisplayNode.textInputPanelNode?.isFocused ?? false - strongSelf.chatDisplayNode.insertSubnode(strongSelf.chatDisplayNode.inputPanelContainerNode, aboveSubnode: strongSelf.chatDisplayNode.inputContextPanelContainer) - if isFocused { - strongSelf.chatDisplayNode.textInputPanelNode?.ensureFocused() - } - - strongSelf.updateChatPresentationInterfaceState(interactive: false) { state in - return state.updatedForceInputCommandsHidden(false) - } + } + }, didDismiss: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl { +// let isFocused = parentController.chatDisplayNode.textInputPanelNode?.isFocused ?? false +// parentController.chatDisplayNode.insertSubnode(parentController.chatDisplayNode.inputPanelContainerNode, aboveSubnode: parentController.chatDisplayNode.inputContextPanelContainer) +// if isFocused { +// parentController.chatDisplayNode.textInputPanelNode?.ensureFocused() +// } + + parentController.updateChatPresentationInterfaceState(interactive: false) { state in + return state.updatedForceInputCommandsHidden(false) + } + } + }, getNavigationController: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl { + return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController + } else { + return parentController?.navigationController as? NavigationController + } + }) + controller.navigationPresentation = .flatModal + parentController.push(controller) + + presentImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + } else if simple { + var isInline = false + var botId = peer.id + var botName = botName + var botAddress = "" + var botVerified = false + if case let .inline(bot) = source { + isInline = true + botId = bot.id + botName = bot.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + botAddress = bot.addressName ?? "" + botVerified = bot.isVerified + } + + let messageActionCallbackDisposable: MetaDisposable + if let parentController = parentController as? ChatControllerImpl { + messageActionCallbackDisposable = parentController.messageActionCallbackDisposable + } else { + messageActionCallbackDisposable = MetaDisposable() + } + + let webViewSignal: Signal + if url.isEmpty { + webViewSignal = context.engine.messages.requestMainWebView(botId: botId, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(presentationData.theme)) + } else { + webViewSignal = context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(presentationData.theme)) + } + + messageActionCallbackDisposable.set(((webViewSignal + |> afterDisposed { + updateProgress() + }) + |> deliverOnMainQueue).start(next: { [weak parentController] result in + guard let parentController else { + 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 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) + }, commit: commit) + }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in + ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: peer.id, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) + }, getNavigationController: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl { + return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController + } else { + return parentController?.navigationController as? NavigationController } - }, getNavigationController: { [weak self] in - return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController }) controller.navigationPresentation = .flatModal - self.push(controller) - } else if simple { - var isInline = false - var botId = peerId - var botName = botName - var botAddress = "" - if case let .inline(bot) = source { - isInline = true - botId = bot.id - botName = bot.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) - botAddress = bot.addressName ?? "" + if let parentController = parentController as? ChatControllerImpl { + parentController.currentWebAppController = controller } + parentController.push(controller) - self.messageActionCallbackDisposable.set(((self.context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(self.presentationData.theme)) - |> afterDisposed { - updateProgress() - }) - |> deliverOnMainQueue).startStrict(next: { [weak self] result in - guard let strongSelf = self else { - return - } - let context = strongSelf.context - let params = WebAppParameters(source: isInline ? .inline : .simple, peerId: peerId, botId: botId, botName: botName, url: result.url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) - let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in - self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) - }, requestSwitchInline: { [weak self] query, chatTypes, completion in - if let strongSelf = self { - if let chatTypes { - let controller = strongSelf.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: strongSelf.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false)) - controller.peerSelected = { [weak self, weak controller] peer, _ in - if let strongSelf = self { - completion() - controller?.dismiss() - strongSelf.controllerInteraction?.activateSwitchInline(peer.id, "@\(botAddress) \(query)", nil) - } - } - strongSelf.push(controller) - } else { - strongSelf.controllerInteraction?.activateSwitchInline(peerId, "@\(botAddress) \(query)", nil) - } - } - }, getNavigationController: { [weak self] in - return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - }) - controller.navigationPresentation = .flatModal - strongSelf.currentWebAppController = controller - strongSelf.push(controller) - }, error: { [weak self] error in - if let strongSelf = self { - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) - } - })) + presentImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + }, error: { [weak parentController] error in + if let parentController { + parentController.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + } + })) + } else { + let messageActionCallbackDisposable: MetaDisposable + if let parentController = parentController as? ChatControllerImpl { + messageActionCallbackDisposable = parentController.messageActionCallbackDisposable } else { - self.messageActionCallbackDisposable.set(((self.context.engine.messages.requestWebView(peerId: peerId, botId: peerId, url: !url.isEmpty ? url : nil, payload: nil, themeParams: generateWebAppThemeParams(self.presentationData.theme), fromMenu: buttonText == "Menu", replyToMessageId: nil, threadId: self.chatLocation.threadId) - |> afterDisposed { - updateProgress() - }) - |> deliverOnMainQueue).startStrict(next: { [weak self] result in - guard let strongSelf = self else { - return + messageActionCallbackDisposable = MetaDisposable() + } + + messageActionCallbackDisposable.set(((context.engine.messages.requestWebView(peerId: peer.id, botId: peer.id, url: !url.isEmpty ? url : nil, payload: nil, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: false, replyToMessageId: nil, threadId: threadId) + |> afterDisposed { + updateProgress() + }) + |> deliverOnMainQueue).startStrict(next: { [weak parentController] result in + guard let parentController else { + return + } + var presentImpl: ((ViewController, Any?) -> Void)? + let params = WebAppParameters(source: .button, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, 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) + }, commit: commit) + }, completion: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl { + parentController.chatDisplayNode.historyNode.scrollToEndOfHistory() } - let context = strongSelf.context - let params = WebAppParameters(source: .generic, peerId: peerId, botId: peerId, botName: botName, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) - let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in - self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) - }, completion: { [weak self] in - self?.chatDisplayNode.historyNode.scrollToEndOfHistory() - }, getNavigationController: { [weak self] in - return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - }) - controller.navigationPresentation = .flatModal - strongSelf.currentWebAppController = controller - strongSelf.push(controller) - }, error: { [weak self] error in - if let strongSelf = self { - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) + }, getNavigationController: { [weak parentController] in + if let parentController = parentController as? ChatControllerImpl { + return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController + } else { + return parentController?.navigationController as? NavigationController } - })) - } + }) + controller.navigationPresentation = .flatModal + if let parentController = parentController as? ChatControllerImpl { + parentController.currentWebAppController = controller + } + parentController.push(controller) + + presentImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + }, error: { [weak parentController] error in + if let parentController { + parentController.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + } + })) } - - var botPeer = EnginePeer(peer) + } + + if skipTermsOfService { + openWebView() + } else { + var botPeer = peer if case let .inline(bot) = source { botPeer = bot } - let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: self.context.sharedContext.accountManager, peerId: botPeer.id) - |> deliverOnMainQueue).startStandalone(next: { [weak self] value in - guard let strongSelf = self else { + let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id) + |> deliverOnMainQueue).startStandalone(next: { [weak parentController] value in + guard let parentController else { return } - + if value { openWebView() } else { - let controller = webAppLaunchConfirmationController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: botPeer, completion: { _ in - let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() + 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) - strongSelf.present(controller, in: .window(.root)) + parentController.present(controller, in: .window(.root)) } }) } - - func presentBotApp(botApp: BotApp, botPeer: EnginePeer, payload: String?, compact: Bool, concealed: Bool = false, commit: @escaping () -> Void = {}) { - guard let peerId = self.chatLocation.peerId else { +} + +public extension ChatControllerImpl { + func openWebApp(buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource) { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } - self.attachmentController?.dismiss(animated: true, completion: nil) + self.chatDisplayNode.dismissInput() - let openBotApp: (Bool, Bool) -> Void = { [weak self] allowWrite, justInstalled in - guard let strongSelf = self else { - return - } - commit() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedTitlePanelContext { - if !$0.contains(where: { - switch $0 { - case .requestInProgress: - return true - default: - return false + self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false) + } + + 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 } - }) { - var updatedContexts = $0 - updatedContexts.append(.requestInProgress) - return updatedContexts.sorted() } - return $0 } - }) - - let updateProgress = { [weak self] in - Queue.mainQueue().async { - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedTitlePanelContext { - if let index = $0.firstIndex(where: { - switch $0 { - case .requestInProgress: - return true - default: - return false - } - }) { - var updatedContexts = $0 - updatedContexts.remove(at: index) - return updatedContexts - } - return $0 - } - }) + 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) + } + } else { + activateSwitchInline() + } + } + + private static func botOpenPeer(context: AccountContext, peerId: EnginePeer.Id, navigation: ChatControllerInteractionNavigateToPeer, navigationController: NavigationController) { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).startStandalone(next: { peer in + guard let peer else { + return + } + switch navigation { + case .default: + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always)) + case let .chat(_, subject, peekData): + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: subject, keepStack: .always, peekData: peekData)) + case .info: + if peer.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) == nil { + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + navigationController.pushViewController(infoController) } } + case let .withBotStartPayload(startPayload): + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), botStart: startPayload)) + case let .withAttachBot(attachBotStart): + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), attachBotStart: attachBotStart)) + case let .withBotApp(botAppStart): + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), botAppStart: botAppStart)) } - - let botAddress = botPeer.addressName ?? "" - strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestAppWebView(peerId: peerId, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: payload, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme), compact: compact, allowWrite: allowWrite) - |> afterDisposed { - updateProgress() + }) + } + + static func botOpenUrl(context: AccountContext, peerId: EnginePeer.Id, controller: ChatControllerImpl?, url: String, concealed: Bool, present: @escaping (ViewController, Any?) -> Void, commit: @escaping () -> Void = {}) { + if let controller { + controller.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) + } else { + let _ = openUserGeneratedUrl(context: context, peerId: peerId, url: url, concealed: concealed, present: { c in + present(c, nil) + }, openResolved: { result in + var navigationController: NavigationController? + if let current = controller?.navigationController as? NavigationController { + navigationController = current + } else if let main = context.sharedContext.mainWindow?.viewController as? NavigationController { + navigationController = main + } + context.sharedContext.openResolvedUrl(result, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + if let navigationController { + ChatControllerImpl.botOpenPeer(context: context, peerId: peer.id, navigation: navigation, navigationController: navigationController) + } + commit() + }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: { peerId, invite, call in + }, + present: { c, a in + present(c, a) + }, dismissInput: { + context.sharedContext.mainWindow?.viewController?.view.endEditing(false) + }, contentContext: nil, progress: nil, completion: nil) }) - |> deliverOnMainQueue).startStrict(next: { [weak self] result in + } + } + + func presentBotApp(botApp: BotApp?, botPeer: EnginePeer, payload: String?, compact: Bool, concealed: Bool = false, commit: @escaping () -> Void = {}) { + guard let peerId = self.chatLocation.peerId else { + return + } + self.attachmentController?.dismiss(animated: true, completion: nil) + + if let botApp { + let openBotApp: (Bool, Bool) -> Void = { [weak self] allowWrite, justInstalled in guard let strongSelf = self else { return } - let context = strongSelf.context - let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize)) - let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in - self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) - }, requestSwitchInline: { [weak self] query, chatTypes, completion in - if let strongSelf = self { - if let chatTypes { - let controller = strongSelf.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: strongSelf.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false)) - controller.peerSelected = { [weak self, weak controller] peer, _ in - if let strongSelf = self { - completion() - controller?.dismiss() - strongSelf.controllerInteraction?.activateSwitchInline(peer.id, "@\(botAddress) \(query)", nil) - } + commit() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if !$0.contains(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false } - strongSelf.push(controller) - } else { - strongSelf.controllerInteraction?.activateSwitchInline(peerId, "@\(botAddress) \(query)", nil) + }) { + var updatedContexts = $0 + updatedContexts.append(.requestInProgress) + return updatedContexts.sorted() } + return $0 } - }, completion: { [weak self] in - self?.chatDisplayNode.historyNode.scrollToEndOfHistory() - }, getNavigationController: { [weak self] in - return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController }) - controller.navigationPresentation = .flatModal - strongSelf.currentWebAppController = controller - strongSelf.push(controller) - if justInstalled { - let content: UndoOverlayContent = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(botPeer.compactDisplayTitle).string, timeout: 5.0, customUndoText: nil) - controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) - } - }, error: { [weak self] error in - if let strongSelf = self { - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) + let updateProgress = { [weak self] in + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if let index = $0.firstIndex(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.remove(at: index) + return updatedContexts + } + return $0 + } + }) + } + } } - })) - } - - let _ = combineLatest( - queue: Queue.mainQueue(), - ApplicationSpecificNotice.getBotGameNotice(accountManager: self.context.sharedContext.accountManager, peerId: botPeer.id), - self.context.engine.messages.attachMenuBots(), - self.context.engine.messages.getAttachMenuBot(botId: botPeer.id, cached: true) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - ).startStandalone(next: { [weak self] noticed, attachMenuBots, attachMenuBot in - guard let self else { - return + + let botAddress = botPeer.addressName ?? "" + strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestAppWebView(peerId: peerId, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: payload, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme), compact: compact, allowWrite: allowWrite) + |> afterDisposed { + updateProgress() + }) + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let strongSelf = self else { + return + } + let context = strongSelf.context + let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize)) + var presentImpl: ((ViewController, Any?) -> Void)? + let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in + ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in + presentImpl?(c, a) + }, commit: commit) + }, requestSwitchInline: { [weak self] query, chatTypes, completion in + ChatControllerImpl.botRequestSwitchInline(context: context, controller: self, peerId: peerId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) + }, completion: { [weak self] in + self?.chatDisplayNode.historyNode.scrollToEndOfHistory() + }, getNavigationController: { [weak self] in + return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController + }) + controller.navigationPresentation = .flatModal + strongSelf.currentWebAppController = controller + strongSelf.push(controller) + + presentImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + + if justInstalled { + let content: UndoOverlayContent = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(botPeer.compactDisplayTitle).string, timeout: 5.0, customUndoText: nil) + controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) + } + }, error: { [weak self] error in + if let strongSelf = self { + strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + } + })) } - var isAttachMenuBotInstalled: Bool? - if let _ = attachMenuBot { - if let _ = attachMenuBots.first(where: { $0.peer.id == botPeer.id && !$0.flags.contains(.notActivated) }) { - isAttachMenuBotInstalled = true - } else { - isAttachMenuBotInstalled = false + let _ = combineLatest( + queue: Queue.mainQueue(), + ApplicationSpecificNotice.getBotGameNotice(accountManager: self.context.sharedContext.accountManager, peerId: botPeer.id), + self.context.engine.messages.attachMenuBots(), + self.context.engine.messages.getAttachMenuBot(botId: botPeer.id, cached: true) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) } - } - - let context = self.context - if !noticed || botApp.flags.contains(.notActivated) || isAttachMenuBotInstalled == false { - if let isAttachMenuBotInstalled, let attachMenuBot { - if !isAttachMenuBotInstalled { - let controller = webAppTermsAlertController(context: context, updatedPresentationData: self.updatedPresentationData, bot: attachMenuBot, completion: { allowWrite in - let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() - let _ = (context.engine.messages.addBotToAttachMenu(botId: botPeer.id, allowWrite: allowWrite) - |> deliverOnMainQueue).startStandalone(error: { _ in - }, completed: { - openBotApp(allowWrite, true) + ).startStandalone(next: { [weak self] noticed, attachMenuBots, attachMenuBot in + guard let self else { + return + } + + var isAttachMenuBotInstalled: Bool? + if let _ = attachMenuBot { + if let _ = attachMenuBots.first(where: { $0.peer.id == botPeer.id && !$0.flags.contains(.notActivated) }) { + isAttachMenuBotInstalled = true + } else { + isAttachMenuBotInstalled = false + } + } + + let context = self.context + if !noticed || botApp.flags.contains(.notActivated) || isAttachMenuBotInstalled == false { + if let isAttachMenuBotInstalled, let attachMenuBot { + if !isAttachMenuBotInstalled { + let controller = webAppTermsAlertController(context: context, updatedPresentationData: self.updatedPresentationData, bot: attachMenuBot, completion: { allowWrite in + let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() + let _ = (context.engine.messages.addBotToAttachMenu(botId: botPeer.id, allowWrite: allowWrite) + |> deliverOnMainQueue).startStandalone(error: { _ in + }, completed: { + openBotApp(allowWrite, true) + }) }) + self.present(controller, in: .window(.root)) + } else { + openBotApp(false, false) + } + } else { + let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: self.updatedPresentationData, peer: botPeer, requestWriteAccess: botApp.flags.contains(.notActivated) && botApp.flags.contains(.requiresWriteAccess), completion: { allowWrite in + let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() + openBotApp(allowWrite, false) + }, showMore: { [weak self] in + if let self { + self.openResolved(result: .peer(botPeer._asPeer(), .info(nil)), sourceMessageId: nil) + } }) self.present(controller, in: .window(.root)) - } else { - openBotApp(false, false) } } else { - let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: self.updatedPresentationData, peer: botPeer, requestWriteAccess: botApp.flags.contains(.notActivated) && botApp.flags.contains(.requiresWriteAccess), completion: { allowWrite in - let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() - openBotApp(allowWrite, false) - }, showMore: { [weak self] in - if let self { - self.openResolved(result: .peer(botPeer._asPeer(), .info(nil)), sourceMessageId: nil) - } - }) - self.present(controller, in: .window(.root)) + openBotApp(false, false) } - } else { - openBotApp(false, false) - } - }) + }) + } else { + self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: botPeer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false) + } } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift index 88a9c0d8aa9..8eacbdcc2a7 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift @@ -186,6 +186,7 @@ extension ChatControllerImpl { audioTrackOffset: nil, audioTrackVolume: nil, audioTrackSamples: nil, + coverImageTimestamp: nil, qualityPreset: nil ) @@ -210,7 +211,7 @@ extension ChatControllerImpl { var fileAttributes: [TelegramMediaFileAttribute] = [] fileAttributes.append(.FileName(fileName: "sticker.webm")) fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) - fileAttributes.append(.Video(duration: animatedImage.duration, size: PixelDimensions(width: 512, height: 512), flags: [], preloadSize: nil)) + fileAttributes.append(.Video(duration: animatedImage.duration, size: PixelDimensions(width: 512, height: 512), flags: [], preloadSize: nil, coverTime: nil)) let previewRepresentations: [TelegramMediaImageRepresentation] = [] // if let thumbnailResource { diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift index d0380195e17..13baa342b4b 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift @@ -222,6 +222,8 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no selfController.interfaceInteraction?.editMessage() }, schedule: { _ in + }, + editPrice: { _ in }, openPremiumPaywall: { [weak selfController] c in guard let selfController else { return @@ -293,7 +295,9 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no }), attachment: false, canSendWhenOnline: sendWhenOnlineAvailable, - forwardMessageIds: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] + forwardMessageIds: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], + canMakePaidContent: false, + currentPrice: nil )), hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, @@ -333,6 +337,7 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no return } selfController.controllerInteraction?.scheduleCurrentMessage(params) + }, editPrice: { _ in }, openPremiumPaywall: { [weak selfController] c in guard let selfController else { return diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 6edc82d3a45..8ecdaba0fa7 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -139,6 +139,7 @@ import MessageUI import PhoneNumberFormat import OwnershipTransferController import OldChannelsController +import BrowserUI public enum ChatControllerPeekActions { case standard @@ -255,6 +256,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var botStart: ChatControllerInitialBotStart? var attachBotStart: ChatControllerInitialAttachBotStart? var botAppStart: ChatControllerInitialBotAppStart? + let mode: ChatControllerPresentationMode let peerDisposable = MetaDisposable() let titleDisposable = MetaDisposable() @@ -305,7 +307,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let temporaryHiddenGalleryMediaDisposable = MetaDisposable() let chatBackgroundNode: WallpaperBackgroundNode - private(set) var controllerInteraction: ChatControllerInteraction? + public private(set) var controllerInteraction: ChatControllerInteraction? var interfaceInteraction: ChatPanelInterfaceInteraction? let messageContextDisposable = MetaDisposable() @@ -409,11 +411,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var historyNavigationStack = ChatHistoryNavigationStack() public let canReadHistory = ValuePromise(true, ignoreRepeated: true) + public let hasBrowserOrAppInFront = Promise(false) var reminderActivity: NSUserActivity? var isReminderActivityEnabled: Bool = false - var canReadHistoryValue = false + var canReadHistoryValue = false { + didSet { + self.computedCanReadHistoryPromise.set(self.canReadHistoryValue) + } + } var canReadHistoryDisposable: Disposable? + var computedCanReadHistoryPromise = ValuePromise(false, ignoreRepeated: true) var themeEmoticonAndDarkAppearancePreviewPromise = Promise<(String?, Bool?)>((nil, nil)) var didSetPresentationData = false @@ -662,6 +670,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.botStart = botStart self.attachBotStart = attachBotStart self.botAppStart = botAppStart + self.mode = mode self.peekData = peekData self.currentChatListFilter = chatListFilter self.chatNavigationStack = chatNavigationStack @@ -1187,6 +1196,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = PremiumIntroScreen(context: strongSelf.context, source: .gift(from: fromPeerId, to: toPeerId, duration: duration, giftCode: nil)) strongSelf.push(controller) return true + case .giftStars: + let controller = strongSelf.context.sharedContext.makeStarsGiftScreen(context: strongSelf.context, message: EngineMessage(message)) + strongSelf.push(controller) + return true case let .giftCode(slug, _, _, _, _, _, _, _, _): strongSelf.openResolved(result: .premiumGiftCode(slug: slug), sourceMessageId: message.id, progress: params.progress) return true @@ -1780,7 +1793,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) { let _ = (strongSelf.context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId]) - |> deliverOnMainQueue).start(next: { [weak strongSelf] files in + |> deliverOnMainQueue).start(next: { [weak strongSelf, weak itemNode] files in guard let strongSelf, let file = files[MessageReaction.starsReactionId] else { return } @@ -1788,6 +1801,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G 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)) + } }) } } @@ -1833,8 +1850,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let context = self?.context, let navigationController = self?.effectiveNavigationController { let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .always).startStandalone() } - }, tapMessage: nil, clickThroughMessage: { [weak self] in - self?.chatDisplayNode.dismissInput() + }, tapMessage: nil, clickThroughMessage: { [weak self] view, location in + self?.chatDisplayNode.dismissInput(view: view, location: location) }, toggleMessagesSelection: { [weak self] ids, value in guard let strongSelf = self, strongSelf.isNodeLoaded else { return @@ -2536,8 +2553,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.effectiveNavigationController, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) { let _ = strongSelf.presentVoiceMessageDiscardAlert(action: { strongSelf.chatDisplayNode.dismissInput() - strongSelf.context.sharedContext.openChatInstantPage(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController) - + if let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType) { + navigationController.pushViewController(controller) + } if case .overlay = strongSelf.presentationInterfaceState.mode { strongSelf.chatDisplayNode.dismissAsOverlay() } @@ -2652,7 +2670,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) |> deliverOnMainQueue).startStandalone(next: { message in - guard let strongSelf = self, let message = message else { + guard let strongSelf = self, let message else { return } @@ -2691,7 +2709,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self, let extendedMedia = paidContent.extendedMedia.first, case let .preview(dimensions, immediateThumbnailData, _) = extendedMedia else { return } - let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: paidContent.amount, startParam: "", extendedMedia: .preview(dimensions:dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: nil), flags: [], version: 0) + var messageId = messageId + if let sourceMessageId = message.forwardInfo?.sourceMessageId { + messageId = sourceMessageId + } + let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: paidContent.amount, startParam: "", extendedMedia: .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: nil), flags: [], version: 0) let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), extendedMedia: paidContent.extendedMedia, inputData: starsInputData, completion: { _ in }) strongSelf.push(controller) @@ -6524,8 +6546,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - self.canReadHistoryDisposable = (combineLatest(context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in - return a && b + + + self.canReadHistoryDisposable = (combineLatest( + context.sharedContext.applicationBindings.applicationInForeground, + self.canReadHistory.get(), + self.hasBrowserOrAppInFront.get() + ) |> map { inForeground, globallyEnabled, hasBrowserOrWebAppInFront in + return inForeground && globallyEnabled && !hasBrowserOrWebAppInFront } |> deliverOnMainQueue).startStrict(next: { [weak self] value in if let strongSelf = self, strongSelf.canReadHistoryValue != value { strongSelf.canReadHistoryValue = value @@ -6556,7 +6584,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = ChatControllerCount.modify { value in return value - 1 } - + let deallocate: () -> Void = { self.historyStateDisposable?.dispose() self.messageIndexDisposable.dispose() @@ -6793,7 +6821,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G func pinnedHistorySignal(anchorMessageId: MessageId?, count: Int) -> Signal { let location: ChatHistoryLocation if let anchorMessageId = anchorMessageId { - location = .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(anchorMessageId), quote: nil), count: count, highlight: false) + location = .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(anchorMessageId), quote: nil), count: count, highlight: false, setupReply: false) } else { location = .Initial(count: count) } @@ -7145,6 +7173,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G // MARK: Nicegram TranslateEnteredMessage (prefetch interlocator language) let _ = getLanguageCode(forChatWith: chatLocation.peerId, context: context).start() // + + if case .standard(.default) = self.mode, !"".isEmpty { + let hasBrowserOrWebAppInFront: Signal = .single([]) + |> then( + self.effectiveNavigationController?.viewControllersSignal ?? .single([]) + ) + |> map { controllers in + if controllers.last is BrowserScreen || controllers.last is AttachmentController { + return true + } else { + return false + } + } + self.hasBrowserOrAppInFront.set(hasBrowserOrWebAppInFront) + } } var returnInputViewFocus = false @@ -7193,9 +7236,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.didAppear = true self.chatDisplayNode.historyNode.experimentalSnapScrollToItem = false - self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest(context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in - return a && b - }) + self.chatDisplayNode.historyNode.canReadHistory.set(self.computedCanReadHistoryPromise.get()) self.chatDisplayNode.loadInputPanels(theme: self.presentationInterfaceState.theme, strings: self.presentationInterfaceState.strings, fontSize: self.presentationInterfaceState.fontSize) @@ -7674,6 +7715,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let _ = self.peekData { self.peekTimerDisposable.set(nil) } + + if case .standard(.default) = self.mode { + self.hasBrowserOrAppInFront.set(.single(false)) + } } func saveInterfaceState(includeScrollState: Bool = true) { @@ -9232,7 +9277,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .chat(textInputState, subject, peekData): dismissWebAppControllers() if case .peer(peerId.id) = strongSelf.chatLocation { - if let subject = subject, case let .message(messageSubject, _, timecode) = subject { + if let subject = subject, case let .message(messageSubject, _, timecode, _) = subject { if case let .id(messageId) = messageSubject { strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: nil))) } @@ -9285,10 +9330,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId.id)) |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in if let strongSelf = self, let peer { - strongSelf.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, compact: botAppStart.compact, concealed: concealed, commit: { - dismissWebAppControllers() + if let botApp = botAppStart.botApp { + strongSelf.presentBotApp(botApp: botApp, botPeer: peer, payload: botAppStart.payload, compact: botAppStart.compact, concealed: concealed, commit: { + dismissWebAppControllers() + commit() + }) + } else { + strongSelf.context.sharedContext.openWebApp(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false) commit() - }) + } } }) default: @@ -9330,7 +9380,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G break default: progress?.set(.single(false)) - self.context.sharedContext.openChatInstantPage(context: self.context, message: message, sourcePeerType: nil, navigationController: navigationController) + if let controller = self.context.sharedContext.makeInstantPageController(context: self.context, message: message, sourcePeerType: nil) { + navigationController.pushViewController(controller) + } return } } diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index e9aa686373b..fbad3ba82c1 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -125,6 +125,14 @@ extension ChatControllerImpl { return } + var deleteAllMessageCount: Signal = .single(nil) + if authors.count == 1 { + deleteAllMessageCount = self.context.engine.messages.searchMessages(location: .peer(peerId: peerId, fromId: authors[0].id, tags: nil, reactions: nil, threadId: self.chatLocation.threadId, minDate: nil, maxDate: nil), query: "", state: nil) + |> map { result, _ -> Int? in + return Int(result.totalCount) + } + } + var signal = combineLatest(authors.map { author in self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id) |> map { result -> (Peer, ChannelParticipant?) in @@ -161,8 +169,8 @@ extension ChatControllerImpl { disposables.set(nil) } - disposables.set((signal - |> deliverOnMainQueue).startStrict(next: { [weak self] authorsAndParticipants in + disposables.set((combineLatest(signal, deleteAllMessageCount) + |> deliverOnMainQueue).startStrict(next: { [weak self] authorsAndParticipants, deleteAllMessageCount in guard let self else { return } @@ -212,6 +220,7 @@ extension ChatControllerImpl { chatPeer: chatPeer, peers: renderedParticipants, messageCount: messageIds.count, + deleteAllMessageCount: deleteAllMessageCount, completion: { [weak self] result in guard let self else { return @@ -259,8 +268,16 @@ extension ChatControllerImpl { disposables.set(nil) } - disposables.set((signal - |> deliverOnMainQueue).startStrict(next: { [weak self] maybeParticipant in + var deleteAllMessageCount: Signal = .single(nil) + do { + deleteAllMessageCount = self.context.engine.messages.getSearchMessageCount(location: .peer(peerId: peerId, fromId: author.id, tags: nil, reactions: nil, threadId: self.chatLocation.threadId, minDate: nil, maxDate: nil), query: "") + |> map { result -> Int? in + return result + } + } + + disposables.set((combineLatest(signal, deleteAllMessageCount) + |> deliverOnMainQueue).startStrict(next: { [weak self] maybeParticipant, deleteAllMessageCount in guard let self else { return } @@ -310,6 +327,7 @@ extension ChatControllerImpl { peer: authorPeer._asPeer() )], messageCount: messageIds.count, + deleteAllMessageCount: deleteAllMessageCount, completion: { [weak self] result in guard let self else { return @@ -322,6 +340,20 @@ extension ChatControllerImpl { } func beginDeleteMessagesWithUndo(messageIds: Set, type: InteractiveMessagesDeletionType) { + var deleteImmediately = false + if case .forEveryone = type { + deleteImmediately = true + } else if case .scheduledMessages = self.presentationInterfaceState.subject { + deleteImmediately = true + } else if case .peer(self.context.account.peerId) = self.chatLocation { + deleteImmediately = true + } + + if deleteImmediately { + let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: type).startStandalone() + return + } + self.chatDisplayNode.historyNode.ignoreMessageIds = Set(messageIds) let undoTitle = self.presentationData.strings.Chat_MessagesDeletedToast_Text(Int32(messageIds.count)) diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index d744a08d982..25b4655028f 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -59,6 +59,7 @@ import ChatInlineSearchResultsListComponent import ComponentDisplayAdapters import ComponentFlow import ChatEmptyNode +import SpaceWarpView final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { let itemNode: OverlayMediaItemNode @@ -109,6 +110,16 @@ private struct ChatControllerNodeDerivedLayoutState { var upperInputPositionBound: CGFloat? } +class ChatNodeContainer: ASDisplayNode { + var contentNode: ASDisplayNode { + return self + } + + override init() { + super.init() + } +} + class HistoryNodeContainer: ASDisplayNode { var isSecret: Bool { didSet { @@ -118,6 +129,10 @@ class HistoryNodeContainer: ASDisplayNode { } } + var contentNode: ASDisplayNode { + return self + } + init(isSecret: Bool) { self.isSecret = isSecret @@ -150,12 +165,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - let contentContainerNode: ASDisplayNode + let wrappingNode: SpaceWarpNode + let contentContainerNode: ChatNodeContainer let contentDimNode: ASDisplayNode let backgroundNode: WallpaperBackgroundNode let historyNode: ChatHistoryListNodeImpl var blurredHistoryNode: ASImageNode? - let historyNodeContainer: ASDisplayNode + let historyNodeContainer: HistoryNodeContainer let loadingNode: ChatLoadingNode private(set) var loadingPlaceholderNode: ChatLoadingPlaceholderNode? @@ -441,7 +457,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.backgroundNode = backgroundNode - self.contentContainerNode = ASDisplayNode() + self.wrappingNode = SpaceWarpNodeImpl() + + self.contentContainerNode = ChatNodeContainer() self.contentDimNode = ASDisplayNode() self.contentDimNode.isUserInteractionEnabled = false self.contentDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.2) @@ -602,7 +620,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let author: Peer if link.isCentered { - author = TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + author = TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) } else { author = accountPeer } @@ -695,7 +713,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.historyNodeContainer = HistoryNodeContainer(isSecret: chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat) - self.historyNodeContainer.addSubnode(self.historyNode) + self.historyNodeContainer.contentNode.addSubnode(self.historyNode) var getContentAreaInScreenSpaceImpl: (() -> CGRect)? var onTransitionEventImpl: ((ContainedViewLayoutTransition) -> Void)? @@ -848,12 +866,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } self.historyNode.enableExtractedBackgrounds = true - self.addSubnode(self.contentContainerNode) - self.contentContainerNode.addSubnode(self.backgroundNode) - self.contentContainerNode.addSubnode(self.historyNodeContainer) + self.addSubnode(self.wrappingNode) + self.wrappingNode.contentNode.addSubnode(self.contentContainerNode) + self.contentContainerNode.contentNode.addSubnode(self.backgroundNode) + self.contentContainerNode.contentNode.addSubnode(self.historyNodeContainer) if let navigationBar = self.navigationBar { - self.contentContainerNode.addSubnode(navigationBar) + self.contentContainerNode.contentNode.addSubnode(navigationBar) } self.inputPanelContainerNode.expansionUpdated = { [weak self] transition in @@ -868,9 +887,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - self.addSubnode(self.inputContextPanelContainer) - self.addSubnode(self.inputPanelContainerNode) - self.addSubnode(self.inputContextOverTextPanelContainer) + self.wrappingNode.contentNode.addSubnode(self.inputContextPanelContainer) + self.wrappingNode.contentNode.addSubnode(self.inputPanelContainerNode) + self.wrappingNode.contentNode.addSubnode(self.inputContextOverTextPanelContainer) self.inputPanelContainerNode.addSubnode(self.inputPanelClippingNode) self.inputPanelContainerNode.addSubnode(self.inputPanelOverlayNode) @@ -878,9 +897,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundSeparatorNode) self.inputPanelBackgroundNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode) - self.addSubnode(self.messageTransitionNode) - self.contentContainerNode.addSubnode(self.navigateButtons) - // MARK: Nicegram if #available(iOS 15.0, *) { nicegramOverlayView.openAiChat = { [weak self] in @@ -898,7 +914,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } if NGSettings.showNicegramButtonInChat { - self.contentContainerNode.addSubnode(self.nicegramOverlayNode) + self.contentContainerNode.contentNode.addSubnode(self.nicegramOverlayNode) } } // @@ -919,8 +935,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.ngBannerNode.isHidden = true // - self.addSubnode(self.presentationContextMarker) - self.contentContainerNode.addSubnode(self.contentDimNode) + self.wrappingNode.contentNode.addSubnode(self.messageTransitionNode) + self.contentContainerNode.contentNode.addSubnode(self.navigateButtons) + self.wrappingNode.contentNode.addSubnode(self.presentationContextMarker) + self.contentContainerNode.contentNode.addSubnode(self.contentDimNode) self.navigationBar?.additionalContentNode.addSubnode(self.titleAccessoryPanelContainer) @@ -1193,9 +1211,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.emptyNode = emptyNode if let inlineSearchResultsView = self.inlineSearchResults?.view { - self.contentContainerNode.view.insertSubview(emptyNode.view, belowSubview: inlineSearchResultsView) + self.contentContainerNode.contentNode.view.insertSubview(emptyNode.view, belowSubview: inlineSearchResultsView) } else { - self.contentContainerNode.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer) + self.contentContainerNode.contentNode.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer) } if let (size, insets) = self.validEmptyNodeLayout { @@ -1254,6 +1272,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { transition = protoTransition } + transition.updateFrame(node: self.wrappingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + self.wrappingNode.update(size: layout.size, cornerRadius: layout.deviceMetrics.screenCornerRadius, transition: ComponentTransition(transition)) + if let statusBar = self.statusBar { switch self.chatPresentationInterfaceState.mode { case .standard: @@ -1270,12 +1291,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - if let historyNodeContainer = self.historyNodeContainer as? HistoryNodeContainer { - let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat - if historyNodeContainer.isSecret != isSecret { - historyNodeContainer.isSecret = isSecret - setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret) - } + let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat + if self.historyNodeContainer.isSecret != isSecret { + self.historyNodeContainer.isSecret = isSecret + setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret) } var previousListBottomInset: CGFloat? @@ -1284,7 +1303,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } self.messageTransitionNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.contentContainerNode.frame = CGRect(origin: CGPoint(), size: layout.size) let isOverlay: Bool @@ -1310,7 +1328,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { animateFromFraction = 1.0 navigationModalFrame = NavigationModalFrame() self.navigationModalFrame = navigationModalFrame - self.insertSubnode(navigationModalFrame, aboveSubnode: self.contentContainerNode) + self.wrappingNode.contentNode.insertSubnode(navigationModalFrame, aboveSubnode: self.contentContainerNode) } if transition.isAnimated, let animateFromFraction = animateFromFraction, animateFromFraction != 1.0 - self.inputPanelContainerNode.expansionFraction { navigationModalFrame.update(layout: layout, transition: .immediate) @@ -1364,7 +1382,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if self.backgroundEffectNode == nil { let backgroundEffectNode = ASDisplayNode() backgroundEffectNode.backgroundColor = self.chatPresentationInterfaceState.theme.chatList.backgroundColor.withAlphaComponent(0.8) - self.insertSubnode(backgroundEffectNode, at: 0) + self.wrappingNode.contentNode.insertSubnode(backgroundEffectNode, at: 0) self.backgroundEffectNode = backgroundEffectNode backgroundEffectNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.backgroundEffectTap(_:)))) } @@ -1376,7 +1394,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { scrollContainerNode.view.contentInsetAdjustmentBehavior = .never } - self.insertSubnode(scrollContainerNode, aboveSubnode: self.backgroundEffectNode!) + self.wrappingNode.contentNode.insertSubnode(scrollContainerNode, aboveSubnode: self.backgroundEffectNode!) self.scrollContainerNode = scrollContainerNode } if self.containerBackgroundNode == nil { @@ -1428,10 +1446,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let containerNode = self.containerNode { self.containerNode = nil containerNode.removeFromSupernode() - self.contentContainerNode.insertSubnode(self.backgroundNode, at: 0) - self.contentContainerNode.insertSubnode(self.historyNodeContainer, aboveSubnode: self.backgroundNode) + self.contentContainerNode.contentNode.insertSubnode(self.backgroundNode, at: 0) + self.contentContainerNode.contentNode.insertSubnode(self.historyNodeContainer, aboveSubnode: self.backgroundNode) if let restrictedNode = self.restrictedNode { - self.contentContainerNode.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer) + self.contentContainerNode.contentNode.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer) } self.navigationBar?.isHidden = false } @@ -1581,7 +1599,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if self.chatImportStatusPanel != importStatusPanelNode { dismissedImportStatusPanelNode = self.chatImportStatusPanel self.chatImportStatusPanel = importStatusPanelNode - self.contentContainerNode.addSubnode(importStatusPanelNode) + self.contentContainerNode.contentNode.addSubnode(importStatusPanelNode) } importStatusPanelHeight = importStatusPanelNode.update(context: self.context, progress: CGFloat(importState.progress), presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: self.chatPresentationInterfaceState.theme, wallpaper: self.chatPresentationInterfaceState.chatWallpaper), fontSize: self.chatPresentationInterfaceState.fontSize, strings: self.chatPresentationInterfaceState.strings, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat, nameDisplayOrder: self.chatPresentationInterfaceState.nameDisplayOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), width: layout.size.width) @@ -1968,7 +1986,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { dismissedOverlayContextPanelNode = self.overlayContextPanelNode self.overlayContextPanelNode = overlayContextPanelNode - self.contentContainerNode.addSubnode(overlayContextPanelNode) + self.contentContainerNode.contentNode.addSubnode(overlayContextPanelNode) immediatelyLayoutOverlayContextPanelAndAnimateAppearance = true } } else if let overlayContextPanelNode = self.overlayContextPanelNode { @@ -2188,7 +2206,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { expandedInputDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) expandedInputDimNode.alpha = 0.0 self.expandedInputDimNode = expandedInputDimNode - self.contentContainerNode.insertSubnode(expandedInputDimNode, aboveSubnode: self.historyNodeContainer) + self.contentContainerNode.contentNode.insertSubnode(expandedInputDimNode, aboveSubnode: self.historyNodeContainer) transition.updateAlpha(node: expandedInputDimNode, alpha: 1.0) expandedInputDimNode.frame = exandedFrame transition.animatePositionAdditive(node: expandedInputDimNode, offset: CGPoint(x: 0.0, y: previousInputPanelOrigin.y - inputPanelOrigin)) @@ -3051,9 +3069,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.skippedShowSearchResultsAsListAnimationOnce = true inlineSearchResultsView.layer.allowsGroupOpacity = true if let emptyNode = self.emptyNode { - self.contentContainerNode.view.insertSubview(inlineSearchResultsView, aboveSubview: emptyNode.view) + self.contentContainerNode.contentNode.view.insertSubview(inlineSearchResultsView, aboveSubview: emptyNode.view) } else { - self.contentContainerNode.view.insertSubview(inlineSearchResultsView, aboveSubview: self.historyNodeContainer.view) + self.contentContainerNode.contentNode.view.insertSubview(inlineSearchResultsView, aboveSubview: self.historyNodeContainer.view) } } inlineSearchResultsTransition.setFrame(view: inlineSearchResultsView, frame: CGRect(origin: CGPoint(), size: layout.size)) @@ -3531,15 +3549,21 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if recognizer.state == .ended { - self.dismissInput() + self.dismissInput(view: self.view, location: recognizer.location(in: self.contentContainerNode.view)) } } - func dismissInput() { + func dismissInput(view: UIView? = nil, location: CGPoint? = nil) { if let _ = self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState { return } + if let view, let location { + if context.sharedContext.immediateExperimentalUISettings.rippleEffect { + self.wrappingNode.triggerRipple(at: self.contentContainerNode.view.convert(location, from: view)) + } + } + switch self.chatPresentationInterfaceState.inputMode { case .none: break @@ -3914,6 +3938,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let textNode = node as? TextAccessibilityOverlayNode { let _ = textNode return result + } else if let _ = node as? LinkHighlightingNode { + return result } } } @@ -4007,7 +4033,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let dropDimNode = ASDisplayNode() dropDimNode.backgroundColor = self.chatPresentationInterfaceState.theme.chatList.backgroundColor.withAlphaComponent(0.35) self.dropDimNode = dropDimNode - self.contentContainerNode.addSubnode(dropDimNode) + self.contentContainerNode.contentNode.addSubnode(dropDimNode) if let (layout, _) = self.validLayout { dropDimNode.frame = CGRect(origin: CGPoint(), size: layout.size) dropDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 847b3f83f1a..f8d76a3a32f 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -615,7 +615,7 @@ extension ChatControllerImpl { payload = botPayload fromAttachMenu = false } - let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: false) + let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: false) let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId) controller.openUrl = { [weak self] url, concealed, commit in diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift index 2a05e3a16bf..8fa4517666f 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift @@ -128,7 +128,7 @@ extension ChatControllerImpl { }))) } - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .message(id: .timestamp(timestamp), highlight: nil, timecode: nil), botStart: nil, mode: .standard(.previewing), params: nil) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .message(id: .timestamp(timestamp), highlight: nil, timecode: nil, setupReply: false), botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index f65710f110b..4f54e76bcf6 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -161,7 +161,15 @@ extension ChatControllerImpl { self.window?.presentInGlobalOverlay(controller) }) } else { - if self.context.sharedContext.applicationBindings.appBuildType == .internal, case .custom(MessageReaction.starsReactionId) = value { + 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 { diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReplies.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReplies.swift index 7a88bbe1c0f..f5cb3b31373 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReplies.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReplies.swift @@ -81,9 +81,9 @@ extension ChatControllerImpl { let subject: ChatControllerSubject? if let atMessageId = atMessageId { - subject = .message(id: .id(atMessageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) + subject = .message(id: .id(atMessageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } else if let index = result.scrollToLowerBoundMessage { - subject = .message(id: .id(index.id), highlight: nil, timecode: nil) + subject = .message(id: .id(index.id), highlight: nil, timecode: nil, setupReply: false) } else { subject = nil } diff --git a/submodules/TelegramUI/Sources/ChatControllerScrollToPointInHistory.swift b/submodules/TelegramUI/Sources/ChatControllerScrollToPointInHistory.swift index 11b92fed0d8..c9f190929a2 100644 --- a/submodules/TelegramUI/Sources/ChatControllerScrollToPointInHistory.swift +++ b/submodules/TelegramUI/Sources/ChatControllerScrollToPointInHistory.swift @@ -11,7 +11,7 @@ import PresentationDataUtils extension ChatControllerImpl { func scrollToEndOfHistory() { - let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .upperBound, quote: nil), anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: 0) + let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .upperBound, quote: nil), anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false, setupReply: false), id: 0) let historyView = preloadedChatHistoryViewForLocation(locationInput, context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: []) let signal = historyView @@ -75,7 +75,7 @@ extension ChatControllerImpl { } func scrollToStartOfHistory() { - let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .lowerBound, quote: nil), anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: 0) + let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .lowerBound, quote: nil), anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false, setupReply: false), id: 0) let historyView = preloadedChatHistoryViewForLocation(locationInput, context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: []) let signal = historyView diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 4296bab4e62..d11043b5ae1 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -12,7 +12,28 @@ import TextFormat import Markdown import Display +struct ChatHistoryEntriesForViewState { + private var messageStableIdToLocalId: [UInt32: Int64] = [:] + + init() { + } + + mutating func messageGroupStableId(messageStableId: UInt32, groupId: Int64, isLocal: Bool) -> Int64 { + if isLocal { + self.messageStableIdToLocalId[messageStableId] = groupId + return groupId + } else { + if let value = self.messageStableIdToLocalId[messageStableId] { + return value + } else { + return groupId + } + } + } +} + func chatHistoryEntriesForView( + currentState: ChatHistoryEntriesForViewState, context: AccountContext, location: ChatLocation, view: MessageHistoryView, @@ -37,9 +58,11 @@ func chatHistoryEntriesForView( cachedData: CachedPeerData?, adMessage: Message?, dynamicAdMessages: [Message] -) -> [ChatHistoryEntry] { +) -> ([ChatHistoryEntry], ChatHistoryEntriesForViewState) { + var currentState = currentState + if historyAppearsCleared { - return [] + return ([], currentState) } var entries: [ChatHistoryEntry] = [] var adminRanks: [PeerId: CachedChannelAdminRank] = [:] @@ -121,8 +144,8 @@ func chatHistoryEntriesForView( } } - var existingGroupStableIds: [UInt32] = [] - var groupBucket: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)] = [] + //var existingGroupStableIds: [UInt32] = [] + //var groupBucket: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)] = [] var count = 0 loop: for entry in view.entries { var message = entry.message @@ -199,7 +222,7 @@ func chatHistoryEntriesForView( } if groupMessages || reverseGroupedMessages { - if !groupBucket.isEmpty && message.groupInfo != groupBucket[0].0.groupInfo { + /*if !groupBucket.isEmpty && message.groupInfo != groupBucket[0].0.groupInfo { if reverseGroupedMessages { groupBucket.reverse() } @@ -215,15 +238,61 @@ func chatHistoryEntriesForView( } } groupBucket.removeAll() - } - if let _ = message.groupInfo { + }*/ + if let messageGroupingKey = message.groupingKey, (groupMessages || reverseGroupedMessages) { let selection: ChatHistoryMessageSelection if let selectedMessages = selectedMessages { selection = .selectable(selected: selectedMessages.contains(message.id)) } else { selection = .none } - groupBucket.append((message, isRead, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }), entry.location)) + + var isCentered = false + if case let .messageOptions(_, _, info) = associatedData.subject, case let .link(link) = info { + isCentered = link.isCentered + } + + let attributes = ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: isCentered, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }) + + let groupStableId = currentState.messageGroupStableId(messageStableId: message.stableId, groupId: messageGroupingKey, isLocal: Namespaces.Message.allLocal.contains(message.id.namespace)) + var found = false + for i in 0 ..< entries.count { + if case let .MessageEntry(currentMessage, _, currentIsRead, currentLocation, currentSelection, currentAttributes) = entries[i], let currentGroupingKey = currentMessage.groupingKey, currentState.messageGroupStableId(messageStableId: currentMessage.stableId, groupId: currentGroupingKey, isLocal: Namespaces.Message.allLocal.contains(currentMessage.id.namespace)) == groupStableId { + found = true + + var currentMessages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)] = [] + + currentMessages.append((currentMessage, currentIsRead, currentSelection, currentAttributes, currentLocation)) + if reverseGroupedMessages { + currentMessages.insert((message, isRead, selection, attributes, entry.location), at: 0) + } else { + currentMessages.append((message, isRead, selection, attributes, entry.location)) + } + + entries[i] = .MessageGroupEntry(groupStableId, currentMessages, presentationData) + } else if case let .MessageGroupEntry(currentGroupStableId, currentMessages, _) = entries[i], currentGroupStableId == groupStableId { + found = true + + var currentMessages = currentMessages + if reverseGroupedMessages { + currentMessages.insert((message, isRead, selection, attributes, entry.location), at: 0) + } else { + currentMessages.append((message, isRead, selection, attributes, entry.location)) + } + entries[i] = .MessageGroupEntry(currentGroupStableId, currentMessages, presentationData) + } + } + if !found { + entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, attributes)) + } + + /*let selection: ChatHistoryMessageSelection + if let selectedMessages = selectedMessages { + selection = .selectable(selected: selectedMessages.contains(message.id)) + } else { + selection = .none + } + groupBucket.append((message, isRead, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }), entry.location))*/ } else { let selection: ChatHistoryMessageSelection if let selectedMessages = selectedMessages { @@ -250,7 +319,7 @@ func chatHistoryEntriesForView( } } - if !groupBucket.isEmpty { + /*if !groupBucket.isEmpty { assert(groupMessages || reverseGroupedMessages) if reverseGroupedMessages { groupBucket.reverse() @@ -266,7 +335,7 @@ func chatHistoryEntriesForView( entries.append(.MessageEntry(message, presentationData, isRead, location, selection, attributes)) } } - } + }*/ if let lowerTimestamp = view.entries.last?.message.timestamp, let upperTimestamp = view.entries.first?.message.timestamp { if let joinMessage { @@ -341,12 +410,12 @@ func chatHistoryEntriesForView( } addedThreadHead = true - if messages.count > 1, let groupInfo = messages[0].groupInfo { + if messages.count > 1, let groupingKey = messages[0].groupingKey { var groupMessages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)] = [] for message in messages { groupMessages.append((message, false, .none, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }), nil)) } - entries.insert(.MessageGroupEntry(groupInfo, groupMessages, presentationData), at: 0) + entries.insert(.MessageGroupEntry(groupingKey, groupMessages, presentationData), at: 0) } else { if !hasTopicCreated { entries.insert(.MessageEntry(messages[0], presentationData, false, nil, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[messages[0].id], isPlaying: false, isCentered: false, authorStoryStats: messages[0].author.flatMap { view.peerStoryStats[$0.id] })), at: 0) @@ -583,8 +652,8 @@ func chatHistoryEntriesForView( } if reverse { - return entries.reversed() + return (entries.reversed(), currentState) } else { - return entries + return (entries, currentState) } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 991ab55f11f..76d9b566168 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -63,7 +63,7 @@ private let historyMessageCount: Int = 44 enum ChatHistoryViewScrollPosition { case unread(index: MessageIndex) case positionRestoration(index: MessageIndex, relativeOffset: CGFloat) - case index(subject: MessageHistoryScrollToSubject, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool, highlight: Bool, displayLink: Bool) + case index(subject: MessageHistoryScrollToSubject, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool, highlight: Bool, displayLink: Bool, setupReply: Bool) } enum ChatHistoryViewUpdateType { @@ -434,7 +434,7 @@ private extension ChatHistoryLocationInput { switch self.content { case .Navigation(index: .upperBound, anchorIndex: .upperBound, count: _, highlight: _): return true - case let .Scroll(subject, anchorIndex, _, _, _, _): + case let .Scroll(subject, anchorIndex, _, _, _, _, _): if case .upperBound = anchorIndex, case .upperBound = subject.index { return true } else { @@ -904,7 +904,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto self.beginReadHistoryManagement() - if let subject = subject, case let .message(messageSubject, highlight, _) = subject { + if let subject = subject, case let .message(messageSubject, highlight, _, setupReply) = subject { let initialSearchLocation: ChatHistoryInitialSearchLocation switch messageSubject { case let .id(id): @@ -917,9 +917,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto initialSearchLocation = .index(MessageIndex.absoluteUpperBound()) } } - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: historyMessageCount, highlight: highlight != nil), id: 0) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: 0) } else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true), id: 0) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true, setupReply: false), id: 0) } else { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Initial(count: historyMessageCount), id: 0) } @@ -1250,7 +1250,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } if let resetScrollingMessageId { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(resetScrollingMessageId.index), quote: nil), anchorIndex: .message(resetScrollingMessageId.index), sourceIndex: .message(resetScrollingMessageId.index), scrollPosition: .top(resetScrollingMessageId.offset), animated: false, highlight: false), id: (self.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(resetScrollingMessageId.index), quote: nil), anchorIndex: .message(resetScrollingMessageId.index), sourceIndex: .message(resetScrollingMessageId.index), scrollPosition: .top(resetScrollingMessageId.offset), animated: false, highlight: false, setupReply: false), id: (self.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } else { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Initial(count: historyMessageCount), id: (self.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } @@ -1321,7 +1321,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let scrollPosition: ChatHistoryViewScrollPosition? if isFirstTime, let messageIndex = messages.first(where: { $0.id == at })?.index { - scrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(messageIndex), quote: quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.text, offset: quote.offset) }), position: .center(.bottom), directionHint: .Down, animated: false, highlight: false, displayLink: false) + scrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(messageIndex), quote: quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.text, offset: quote.offset) }), position: .center(.bottom), directionHint: .Down, animated: false, highlight: false, displayLink: false, setupReply: false) isFirstTime = false } else { scrollPosition = nil @@ -1367,8 +1367,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto var scrollPositionValue: ChatHistoryViewScrollPosition? if let location { switch location.content { - case let .Scroll(subject, _, _, scrollPosition, animated, highlight): - scrollPositionValue = .index(subject: subject, position: scrollPosition, directionHint: .Up, animated: animated, highlight: highlight, displayLink: false) + case let .Scroll(subject, _, _, scrollPosition, animated, highlight, setupReply): + scrollPositionValue = .index(subject: subject, position: scrollPosition, directionHint: .Up, animated: animated, highlight: highlight, displayLink: false, setupReply: setupReply) default: break } @@ -1449,6 +1449,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } |> distinctUntilChanged + let chatHistoryEntriesForViewState = Atomic(value: ChatHistoryEntriesForViewState()) + let animatedEmojiStickers: Signal<[String: [StickerPackItem]], NoError> = context.animatedEmojiStickers let additionalAnimatedEmojiStickers = context.additionalAnimatedEmojiStickers @@ -1703,7 +1705,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(anchorIndex), anchorIndex: .message(anchorIndex), count: historyMessageCount, highlight: false), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } } else { - if let subject = subject, case let .message(messageSubject, highlight, _) = subject { + if let subject = subject, case let .message(messageSubject, highlight, _, setupReply) = subject { let initialSearchLocation: ChatHistoryInitialSearchLocation switch messageSubject { case let .id(id): @@ -1716,9 +1718,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto initialSearchLocation = .index(.absoluteUpperBound()) } } - strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: historyMessageCount, highlight: highlight != nil), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId { - strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true, setupReply: false), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } else if var chatHistoryLocation = strongSelf.chatHistoryLocationValue { chatHistoryLocation.id += 1 strongSelf.chatHistoryLocationValue = chatHistoryLocation @@ -1888,7 +1890,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto includeEmbeddedSavedChatInfo = true } - let filteredEntries = chatHistoryEntriesForView( + let previousChatHistoryEntriesForViewState = chatHistoryEntriesForViewState.with({ $0 }) + + let (filteredEntries, updatedChatHistoryEntriesForViewState) = chatHistoryEntriesForView( + currentState: previousChatHistoryEntriesForViewState, context: context, location: chatLocation, view: view, @@ -1917,6 +1922,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let lastHeaderId = filteredEntries.last.flatMap { listMessageDateHeaderId(timestamp: $0.index.timestamp) } ?? 0 let processedView = ChatHistoryView(originalView: view, filteredEntries: filteredEntries, associatedData: associatedData, lastHeaderId: lastHeaderId, id: id, locationInput: update.2, ignoreMessagesInTimestampRange: update.3, ignoreMessageIds: update.4) let previousValueAndVersion = previousView.swap((processedView, update.1, selectedMessages, allAdMessages.version)) + let _ = chatHistoryEntriesForViewState.swap(updatedChatHistoryEntriesForViewState) let previous = previousValueAndVersion?.0 let previousSelectedMessages = previousValueAndVersion?.2 @@ -1926,10 +1932,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if scrollPosition == nil, let originalScrollPosition = originalScrollPosition { switch originalScrollPosition { - case let .index(subject, position, _, _, highlight, displayLink): + case let .index(subject, position, _, _, highlight, displayLink, setupReply): if case .upperBound = subject.index { if let previous = previous, previous.filteredEntries.isEmpty { - updatedScrollPosition = .index(subject: subject, position: position, directionHint: .Down, animated: false, highlight: highlight, displayLink: displayLink) + updatedScrollPosition = .index(subject: subject, position: position, directionHint: .Down, animated: false, highlight: highlight, displayLink: displayLink, setupReply: setupReply) } } default: @@ -1979,7 +1985,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if let strongSelf = self, case .default = source { strongSelf.toLang = translateToLanguage if strongSelf.appliedScrollToMessageId == nil, let scrollToMessageId = scrollToMessageId { - updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(scrollToMessageId), quote: nil), position: .center(.top), directionHint: .Up, animated: true, highlight: false, displayLink: true) + updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(scrollToMessageId), quote: nil), position: .center(.top), directionHint: .Up, animated: true, highlight: false, displayLink: true, setupReply: false) scrollAnimationCurve = .Spring(duration: 0.4) } else { let wasPlaying = strongSelf.appliedPlayingMessageId != nil @@ -2009,7 +2015,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } }) if currentIsVisible && nextIsVisible && currentlyPlayingVideo { - updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(currentlyPlayingMessageId), quote: nil), position: .center(.bottom), directionHint: .Up, animated: true, highlight: true, displayLink: true) + updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(currentlyPlayingMessageId), quote: nil), position: .center(.bottom), directionHint: .Up, animated: true, highlight: true, displayLink: true, setupReply: false) scrollAnimationCurve = .Spring(duration: 0.4) } } @@ -2071,7 +2077,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } if fillsScreen, let firstNonAdIndex = firstNonAdIndex, previousNumAds == 0, updatedNumAds != 0 { - updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(firstNonAdIndex), quote: nil), position: .top(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false) + updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(firstNonAdIndex), quote: nil), position: .top(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false, setupReply: false) disableAnimations = true } } @@ -2095,7 +2101,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if case let .MessageEntry(message, _, _, _, _, _) = entry { if message.adAttribute == nil { if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case .joinedChannel = action.action { - updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(message.index), quote: nil), position: .top(0.0), directionHint: .Up, animated: true, highlight: false, displayLink: false) + updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(message.index), quote: nil), position: .top(0.0), directionHint: .Up, animated: true, highlight: false, displayLink: false, setupReply: false) } break } @@ -3156,7 +3162,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto currentMessage = messages.first?.0 } if let message = currentMessage, let _ = self.anchorMessageInCurrentHistoryView() { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(message.index), quote: nil), anchorIndex: .message(message.index), sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(message.index), quote: nil), anchorIndex: .message(message.index), sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false, setupReply: false), id: self.takeNextHistoryLocationId()) } } } @@ -3185,14 +3191,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } if let currentMessage = currentMessage { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(currentMessage.index), quote: nil), anchorIndex: .message(currentMessage.index), sourceIndex: .upperBound, scrollPosition: .top(0.0), animated: true, highlight: true), id: self.takeNextHistoryLocationId()) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(currentMessage.index), quote: nil), anchorIndex: .message(currentMessage.index), sourceIndex: .upperBound, scrollPosition: .top(0.0), animated: true, highlight: true, setupReply: false), id: self.takeNextHistoryLocationId()) } } } public func scrollToStartOfHistory() { self.beganDragging?() - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .lowerBound, quote: nil), anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .lowerBound, quote: nil), anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false, setupReply: false), id: self.takeNextHistoryLocationId()) } public func scrollToEndOfHistory() { @@ -3201,13 +3207,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto case let .known(value) where value <= CGFloat.ulpOfOne: break default: - let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .upperBound, quote: nil), anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) + let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .upperBound, quote: nil), anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false, setupReply: false), id: self.takeNextHistoryLocationId()) self.chatHistoryLocationValue = locationInput } } - public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, quote: (string: String, offset: Int?)? = nil, scrollPosition: ListViewScrollPosition = .center(.bottom)) { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(toIndex), quote: quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight), id: self.takeNextHistoryLocationId()) + public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, quote: (string: String, offset: Int?)? = nil, scrollPosition: ListViewScrollPosition = .center(.bottom), setupReply: Bool = false) { + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(toIndex), quote: quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }, setupReply: setupReply), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight, setupReply: setupReply), id: self.takeNextHistoryLocationId()) } public func anchorMessageInCurrentHistoryView() -> Message? { diff --git a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift index 5f78baacda6..e70ce3a7bd8 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift @@ -55,9 +55,9 @@ func chatHistoryViewForLocation( if scheduled { var first = true var chatScrollPosition: ChatHistoryViewScrollPosition? - if case let .Scroll(subject, _, sourceIndex, position, animated, highlight) = location.content { + if case let .Scroll(subject, _, sourceIndex, position, animated, highlight, setupReply) = location.content { let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > subject.index ? .Down : .Up - chatScrollPosition = .index(subject: subject, position: position, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false) + chatScrollPosition = .index(subject: subject, position: position, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false, setupReply: setupReply) } return account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in @@ -165,7 +165,7 @@ func chatHistoryViewForLocation( if tag == nil, case let .replyThread(message) = chatLocation, message.isForumPost, view.maxReadIndex == nil { if case let .message(index) = view.anchorIndex { - scrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(index), quote: nil), position: .bottom(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false) + scrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(index), quote: nil), position: .bottom(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false, setupReply: false) } } @@ -227,7 +227,7 @@ func chatHistoryViewForLocation( return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) } } - case let .InitialSearch(searchLocationSubject, count, highlight): + case let .InitialSearch(searchLocationSubject, count, highlight, setupReply): var preloaded = false var fadeIn = false @@ -280,7 +280,7 @@ func chatHistoryViewForLocation( preloaded = true - return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(subject: MessageHistoryScrollToSubject(index: anchorIndex, quote: searchLocationSubject.quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }), position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false), flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) + return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(subject: MessageHistoryScrollToSubject(index: anchorIndex, quote: searchLocationSubject.quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }, setupReply: setupReply), position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false, setupReply: setupReply), flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) } } case let .Navigation(index, anchorIndex, count, _): @@ -297,9 +297,9 @@ func chatHistoryViewForLocation( } return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) } - case let .Scroll(subject, anchorIndex, sourceIndex, scrollPosition, animated, highlight): + case let .Scroll(subject, anchorIndex, sourceIndex, scrollPosition, animated, highlight, setupReply): let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > subject.index ? .Down : .Up - let chatScrollPosition = ChatHistoryViewScrollPosition.index(subject: subject, position: scrollPosition, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false) + let chatScrollPosition = ChatHistoryViewScrollPosition.index(subject: subject, position: scrollPosition, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false, setupReply: setupReply) var first = true return account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, index: subject.index, anchorIndex: anchorIndex, count: 128, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData, useRootInterfaceStateForThread: useRootInterfaceStateForThread) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in @@ -413,7 +413,7 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea case .automatic: if let atMessageId = atMessageId { input = ChatHistoryLocationInput( - content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(atMessageId), quote: nil), count: 40, highlight: true), + content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(atMessageId), quote: nil), count: 40, highlight: true, setupReply: false), id: 0 ) } else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 0e33b8d06ba..9d582897804 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -418,7 +418,7 @@ func messageMediaEditingOptions(message: Message) -> MessageMediaEditingOptions return [] case .Animated: break - case let .Video(_, _, flags, _): + case let .Video(_, _, flags, _, _): if flags.contains(.instantRoundVideo) { return [] } else { @@ -1757,13 +1757,17 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState var clearCacheAsDelete = false if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, !isMigrated { var views: Int = 0 + var forwards: Int = 0 for attribute in message.attributes { if let attribute = attribute as? ViewCountMessageAttribute { views = attribute.count } + if let attribute = attribute as? ForwardCountMessageAttribute { + forwards = attribute.count + } } - if infoSummaryData.canViewStats, views >= 100 { + if infoSummaryData.canViewStats, forwards >= 1 || views >= 100 { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextViewStats, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.actionSheet.primaryTextColor) }, action: { c, _ in @@ -1832,7 +1836,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState guard let peer = messages[0].peers[messages[0].id.peerId] else { return } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messages[0].id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), useExisting: true)) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messages[0].id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), useExisting: true)) }) }))) } @@ -3470,11 +3474,11 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus } let avatarsSize = self.avatarsNode.update(context: self.item.context, content: avatarsContent, itemSize: CGSize(width: 24.0, height: 24.0), customSpacing: 10.0, animated: false, synchronousLoad: true) - self.avatarsNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - 12.0 - avatarsSize.width, y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize) + self.avatarsNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width - sideInset - 2.0 - avatarsSize.width), y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize) transition.updateAlpha(node: self.avatarsNode, alpha: self.currentStats == nil ? 0.0 : 1.0) let placeholderAvatarsSize = self.placeholderAvatarsNode.update(context: self.item.context, content: placeholderAvatarsContent, itemSize: CGSize(width: 24.0, height: 24.0), customSpacing: 10.0, animated: false, synchronousLoad: true) - self.placeholderAvatarsNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - 8.0 - placeholderAvatarsSize.width, y: floor((size.height - placeholderAvatarsSize.height) / 2.0)), size: placeholderAvatarsSize) + self.placeholderAvatarsNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - 2.0 - placeholderAvatarsSize.width, y: floor((size.height - placeholderAvatarsSize.height) / 2.0)), size: placeholderAvatarsSize) transition.updateAlpha(node: self.placeholderAvatarsNode, alpha: self.currentStats == nil ? 1.0 : 0.0) transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index add22dd404e..b339d07840c 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -267,7 +267,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe switch item.content { case let .peer(peerData): if let message = peerData.messages.first { - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerData.peer.peerId), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), botStart: nil, mode: .standard(.previewing), params: nil) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerData.peer.peerId), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(content: .list([]))), gesture: gesture) presentInGlobalOverlay(contextController) diff --git a/submodules/TelegramUI/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Sources/ChatThemeScreen.swift index 78043bd9aef..7f3a277dac6 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatThemeScreen.swift @@ -20,7 +20,7 @@ import TooltipUI import AnimatedStickerNode import TelegramAnimatedStickerNode import ShimmerEffect -import WebUI +import AttachmentUI private struct ThemeSettingsThemeEntry: Comparable, Identifiable { let index: Int diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index 5b1f86bbe67..a598a5a4a1f 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -169,7 +169,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection strongSelf.updateTitle() }) - case let .premiumGifting(birthdays, selectToday): + case let .premiumGifting(birthdays, selectToday, _): if let birthdays, selectToday { let today = Calendar(identifier: .gregorian).component(.day, from: Date()) var todayPeers: [EnginePeer.Id] = [] @@ -256,13 +256,13 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection self.rightNavigationButton = rightNavigationButton self.navigationItem.rightBarButtonItem = self.rightNavigationButton } - case .premiumGifting: + case let .premiumGifting(_, _, hasActions): let maxCount: Int32 = self.limit ?? 10 var count = 0 if case let .contacts(contactsNode) = self.contactsNode.contentNode { count = contactsNode.selectionState?.selectedPeerIndices.count ?? 0 } - self.titleView.title = CounterControllerTitle(title: self.presentationData.strings.Premium_Gift_ContactSelection_Title, counter: "\(count)/\(maxCount)") + self.titleView.title = CounterControllerTitle(title: hasActions ? self.presentationData.strings.Premium_Gift_ContactSelection_Title : self.presentationData.strings.Stars_Purchase_GiftStars, counter: "\(count)/\(maxCount)") case .requestedUsersSelection: let maxCount: Int32 = self.limit ?? 10 var count = 0 diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index 93863b99889..2c748bf0090 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -195,10 +195,10 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { } else { let displayTopPeers: ContactListPresentation.TopPeers var selectedPeers: [EnginePeer.Id] = [] - if case let .premiumGifting(birthdays, selectToday) = mode { + if case let .premiumGifting(birthdays, selectToday, hasActions) = mode { if let birthdays { let today = Calendar(identifier: .gregorian).component(.day, from: Date()) - var sections: [(String, [EnginePeer.Id])] = [] + var sections: [(String, [EnginePeer.Id], Bool)] = [] var todayPeers: [EnginePeer.Id] = [] var yesterdayPeers: [EnginePeer.Id] = [] var tomorrowPeers: [EnginePeer.Id] = [] @@ -217,13 +217,13 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { } if !todayPeers.isEmpty { - sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayToday, todayPeers)) + sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayToday, todayPeers, hasActions)) } if !yesterdayPeers.isEmpty { - sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayYesterday, yesterdayPeers)) + sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayYesterday, yesterdayPeers, hasActions)) } if !tomorrowPeers.isEmpty { - sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayTomorrow, tomorrowPeers)) + sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayTomorrow, tomorrowPeers, hasActions)) } displayTopPeers = .custom(sections) diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index 7db9e4a61ea..cabf9baa3e0 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -17,6 +17,7 @@ import ChatSendMessageActionUI class ContactSelectionControllerImpl: ViewController, ContactSelectionController, PresentableController, AttachmentContainable { private let context: AccountContext + private let mode: ContactSelectionControllerMode private let autoDismiss: Bool fileprivate var contactsNode: ContactSelectionControllerNode { @@ -35,7 +36,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController private let index: PeerNameIndex = .lastNameFirst private let titleProducer: (PresentationStrings) -> String - private let options: [ContactListAdditionalOption] + private let options: Signal<[ContactListAdditionalOption], NoError> private let displayDeviceContacts: Bool private let displayCallIcons: Bool private let multipleSelection: Bool @@ -88,11 +89,13 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController var cancelPanGesture: () -> Void = { } var isContainerPanning: () -> Bool = { return false } var isContainerExpanded: () -> Bool = { return false } + var isMinimized: Bool = false var getCurrentSendMessageContextMediaPreview: (() -> ChatSendMessageContextScreenMediaPreview?)? init(_ params: ContactSelectionControllerParams) { self.context = params.context + self.mode = params.mode self.autoDismiss = params.autoDismiss self.titleProducer = params.title self.options = params.options @@ -206,7 +209,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController } override func loadDisplayNode() { - self.displayNode = ContactSelectionControllerNode(context: self.context, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection, requirePhoneNumbers: self.requirePhoneNumbers) + self.displayNode = ContactSelectionControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection, requirePhoneNumbers: self.requirePhoneNumbers) self._ready.set(self.contactsNode.contactListNode.ready) self.contactsNode.navigationBar = self.navigationBar @@ -452,29 +455,6 @@ final class ContactsPickerContext: AttachmentMediaPickerContext { return .single(0) } } - - var caption: Signal { - return .single(nil) - } - - var hasCaption: Bool { - return false - } - - var captionIsAboveMedia: Signal { - return .single(false) - } - - func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { - } - - public var loadingProgress: Signal { - return .single(nil) - } - - public var mainButtonState: Signal { - return .single(nil) - } init(controller: ContactSelectionControllerImpl) { self.controller = controller diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index cface4f8c96..685113732ec 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -55,7 +55,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { var searchContainerNode: ContactsSearchContainerNode? - init(context: AccountContext, presentationData: PresentationData, options: [ContactListAdditionalOption], displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool) { + init(context: AccountContext, mode: ContactSelectionControllerMode, presentationData: PresentationData, options: Signal<[ContactListAdditionalOption], NoError>, displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool) { self.context = context self.presentationData = presentationData self.displayDeviceContacts = displayDeviceContacts @@ -65,10 +65,55 @@ final class ContactSelectionControllerNode: ASDisplayNode { if requirePhoneNumbers { filters.append(.excludeWithoutPhoneNumbers) } + if case .starsGifting = mode { + filters.append(.excludeBots) + } self.filters = filters + let displayTopPeers: ContactListPresentation.TopPeers + if case let .starsGifting(birthdays, hasActions) = mode { + if let birthdays { + let today = Calendar(identifier: .gregorian).component(.day, from: Date()) + var sections: [(String, [EnginePeer.Id], Bool)] = [] + var todayPeers: [EnginePeer.Id] = [] + var yesterdayPeers: [EnginePeer.Id] = [] + var tomorrowPeers: [EnginePeer.Id] = [] + + for (peerId, birthday) in birthdays { + if birthday.day == today { + todayPeers.append(peerId) + } else if birthday.day == today - 1 || birthday.day > today + 5 { + yesterdayPeers.append(peerId) + } else if birthday.day == today + 1 || birthday.day < today + 5 { + tomorrowPeers.append(peerId) + } + } + + if !todayPeers.isEmpty { + sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayToday, todayPeers, hasActions)) + } + if !yesterdayPeers.isEmpty { + sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayYesterday, yesterdayPeers, hasActions)) + } + if !tomorrowPeers.isEmpty { + sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayTomorrow, tomorrowPeers, hasActions)) + } + + displayTopPeers = .custom(sections) + } else { + displayTopPeers = .recent + } + } else { + displayTopPeers = .none + } + + let presentation: Signal = options + |> map { options in + return .natural(options: options, includeChatList: false, topPeers: displayTopPeers) + } + var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? - self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: .single(.natural(options: options, includeChatList: false, topPeers: .none)), filters: filters, onlyWriteable: false, isGroupInvitation: false, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in + self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: presentation, filters: filters, onlyWriteable: false, isGroupInvitation: false, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in contextActionImpl?(peer, node, gesture, nil) } : nil, multipleSelection: multipleSelection) @@ -262,7 +307,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { } else { categories.insert(.global) } - self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, updatedPresentationData: (self.presentationData, self.presentationDataPromise.get()), onlyWriteable: false, categories: categories, addContact: nil, openPeer: { [weak self] peer in + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, updatedPresentationData: (self.presentationData, self.presentationDataPromise.get()), onlyWriteable: false, categories: categories, filters: self.filters, addContact: nil, openPeer: { [weak self] peer in if let strongSelf = self { var updated = false strongSelf.contactListNode.updateSelectionState { state -> ContactListNodeGroupSelectionState? in diff --git a/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift b/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift index 896e5879da3..ca8e9f055cf 100644 --- a/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift +++ b/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift @@ -585,7 +585,7 @@ private func fetchVideoStickerRepresentation(account: Account, resource: MediaRe private func fetchPreparedPatternWallpaperRepresentation(resource: MediaResource, resourceData: MediaResourceData, representation: CachedPreparedPatternWallpaperRepresentation) -> Signal { return Signal({ subscriber in if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { - if let unpackedData = TGGUnzipData(data, 2 * 1024 * 1024), let data = prepareSvgImage(unpackedData) { + if let unpackedData = TGGUnzipData(data, 2 * 1024 * 1024), let data = prepareSvgImage(unpackedData, true) { let path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max))" let url = URL(fileURLWithPath: path) let _ = try? data.write(to: url) @@ -600,7 +600,7 @@ private func fetchPreparedPatternWallpaperRepresentation(resource: MediaResource private func fetchPreparedSvgRepresentation(resource: MediaResource, resourceData: MediaResourceData, representation: CachedPreparedSvgRepresentation) -> Signal { return Signal({ subscriber in if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { - if let data = prepareSvgImage(data) { + if let data = prepareSvgImage(data, true) { let path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max))" let url = URL(fileURLWithPath: path) let _ = try? data.write(to: url) diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift index 6d7b4d11505..d41b9923b6c 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift @@ -260,7 +260,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode } imageDimensions = externalReference.content?.dimensions?.cgSize if externalReference.type == "gif", let thumbnailResource = externalReference.thumbnail?.resource, let content = externalReference.content, let dimensions = content.dimensions { - videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil)]) + videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)]) imageResource = nil } diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index 452a8deebfe..25480b6b890 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -121,11 +121,11 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam controller.updateTextInputState(updateTextInputState) } var popAndComplete = true - if let subject = params.subject, case let .message(messageSubject, highlight, timecode) = subject { + if let subject = params.subject, case let .message(messageSubject, highlight, timecode, setupReply) = subject { if case let .id(messageId) = messageSubject { let navigationController = params.navigationController let animated = params.animated - controller.navigateToMessage(messageLocation: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: (highlight?.quote).flatMap { quote in NavigateToMessageParams.Quote(string: quote.string, offset: quote.offset) })), animated: isFirst, completion: { [weak navigationController, weak controller] in + controller.navigateToMessage(messageLocation: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: (highlight?.quote).flatMap { quote in NavigateToMessageParams.Quote(string: quote.string, offset: quote.offset) }, setupReply: setupReply)), animated: isFirst, completion: { [weak navigationController, weak controller] in if let navigationController = navigationController, let controller = controller { let _ = navigationController.popToViewController(controller, animated: animated) } @@ -373,7 +373,7 @@ public func navigateToForumThreadImpl(context: AccountContext, peerId: EnginePee context: context, chatLocation: .replyThread(result.message), chatLocationContextHolder: result.contextHolder, - subject: messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) }, + subject: messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) }, activateInput: actualActivateInput, keepStack: keepStack, scrollToEndIfExists: scrollToEndIfExists, diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 30f7ee7b64b..cade6b33775 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -24,6 +24,7 @@ import WebsiteType import GalleryData import StoryContainerScreen import WallpaperGalleryScreen +import BrowserUI func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { var story: TelegramMediaStory? @@ -230,7 +231,18 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { params.present(controller, nil) } else if let rootController = params.navigationController?.view.window?.rootViewController { let proceed = { - presentDocumentPreviewController(rootController: rootController, theme: presentationData.theme, strings: presentationData.strings, postbox: params.context.account.postbox, file: file, canShare: !params.message.isCopyProtected()) + if params.context.sharedContext.immediateExperimentalUISettings.browserExperiment && BrowserScreen.supportedDocumentMimeTypes.contains(file.mimeType) { + let subject: BrowserScreen.Subject + if file.mimeType == "application/pdf" { + subject = .pdfDocument(file: file) + } else { + subject = .document(file: file) + } + let controller = BrowserScreen(context: params.context, subject: subject) + 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()) + } } if file.mimeType.contains("image/svg") { let presentationData = params.context.sharedContext.currentPresentationData.with { $0 } @@ -369,13 +381,16 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { return false } -func openChatInstantPageImpl(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?, navigationController: NavigationController) { - if let (webpage, anchor) = instantPageAndAnchor(message: message) { - let sourceLocation = InstantPageSourceLocation(userLocation: .peer(message.id.peerId), peerType: sourcePeerType ?? .channel) - - let pageController = InstantPageController(context: context, webPage: webpage, sourceLocation: sourceLocation, anchor: anchor) - navigationController.pushViewController(pageController) +func makeInstantPageControllerImpl(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController? { + guard let (webpage, anchor) = instantPageAndAnchor(message: message) else { + return nil } + let sourceLocation = InstantPageSourceLocation(userLocation: .peer(message.id.peerId), peerType: sourcePeerType ?? .channel) + return makeInstantPageControllerImpl(context: context, webPage: webpage, anchor: anchor, sourceLocation: sourceLocation) +} + +func makeInstantPageControllerImpl(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController { + return BrowserScreen(context: context, subject: .instantPage(webPage: webPage, anchor: anchor, sourceLocation: sourceLocation)) } func openChatWallpaperImpl(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) { diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 9bf678e4bf9..c92885e83d2 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -34,6 +34,7 @@ import StoryContainerScreen import WallpaperGalleryScreen import TelegramStringFormatting import TextFormat +import BrowserUI private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -207,7 +208,7 @@ func openResolvedUrlImpl( dismissInput() navigationController?.pushViewController(controller) case let .channelMessage(peer, messageId, timecode): - openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode), peekData: nil)) + openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode, setupReply: false), peekData: nil)) case let .replyThreadMessage(replyThreadMessage, messageId): if let navigationController = navigationController, let effectiveMessageId = replyThreadMessage.effectiveMessageId { let _ = ChatControllerImpl.openMessageReplies(context: context, navigationController: navigationController, present: { c, a in @@ -247,8 +248,10 @@ func openResolvedUrlImpl( } }) present(controller, nil) - case let .instantView(webpage, anchor): - navigationController?.pushViewController(InstantPageController(context: context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .channel), anchor: anchor)) + case let .instantView(webPage, anchor): + let sourceLocation = InstantPageSourceLocation(userLocation: .other, peerType: .channel) + let browserController = context.sharedContext.makeInstantPageController(context: context, webPage: webPage, anchor: anchor, sourceLocation: sourceLocation) + navigationController?.pushViewController(browserController) case let .join(link): dismissInput() @@ -653,6 +656,14 @@ func openResolvedUrlImpl( if let navigationController = navigationController { navigationController.pushViewController(controller, animated: true) } + case let .starsTopup(amount): + 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) + } + } case let .joinVoiceChat(peerId, invite): let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { peer in @@ -1076,7 +1087,7 @@ func openResolvedUrlImpl( guard let peer else { return } - openPeer(peer, .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), peekData: nil)) + openPeer(peer, .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), peekData: nil)) if case let .chat(peerId, _, _) = urlContext, peerId == messageId.peerId { dismissImpl?() } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index e70e0bc209f..68565fec965 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -165,23 +165,32 @@ private func extractNicegramDeeplink(from link: String) -> String? { } // -func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void) { +// MARK: Nicegram, skipNicegramProcessing added +func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, skipNicegramProcessing: Bool = false, dismissInput: @escaping () -> Void) { // MARK: Nicegram - if let nicegramDeeplink = extractNicegramDeeplink(from: url) { - openExternalUrlImpl(context: context, urlContext: urlContext, url: nicegramDeeplink, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: dismissInput) - return - } - - let walletDeeplinksManager = NicegramWallet.DeeplinksModule.shared.deeplinksManager() - if walletDeeplinksManager.handle(url) { - return - } - - let nicegramHandler = NGDeeplinkHandler( - tgAccountContext: context, - navigationController: navigationController - ) - if nicegramHandler.handle(url: url) { + if !skipNicegramProcessing { + if let nicegramDeeplink = extractNicegramDeeplink(from: url) { + openExternalUrlImpl(context: context, urlContext: urlContext, url: nicegramDeeplink, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: dismissInput) + return + } + + Task { @MainActor in + let walletDeeplinksManager = NicegramWallet.DeeplinksModule.shared.deeplinksManager() + if await walletDeeplinksManager.handle(url) { + return + } + + let nicegramHandler = NGDeeplinkHandler( + tgAccountContext: context, + navigationController: navigationController + ) + if nicegramHandler.handle(url: url) { + return + } + + openExternalUrlImpl(context: context, urlContext: urlContext, url: url, forceExternal: forceExternal, presentationData: presentationData, navigationController: navigationController, skipNicegramProcessing: true, dismissInput: dismissInput) + } + return } // @@ -297,15 +306,9 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur |> deliverOnMainQueue).startStandalone(next: handleResolvedUrl) } - if context.sharedContext.immediateExperimentalUISettings.browserExperiment { - if let scheme = parsedUrl.scheme, (scheme == "tg" || scheme == context.sharedContext.applicationBindings.appSpecificScheme) { - if parsedUrl.host == "ipfs" { - if let value = URL(string: "ipfs:/" + parsedUrl.path) { - parsedUrl = value - } - } - } else if let scheme = parsedUrl.scheme, scheme == "https", parsedUrl.host == "t.me", parsedUrl.path.hasPrefix("/ipfs/") { - if let value = URL(string: "ipfs://" + String(parsedUrl.path[parsedUrl.path.index(parsedUrl.path.startIndex, offsetBy: "/ipfs/".count)...])) { + if let scheme = parsedUrl.scheme, (scheme == "tg" || scheme == context.sharedContext.applicationBindings.appSpecificScheme) { + if parsedUrl.host == "tonsite" { + if let value = URL(string: "tonsite:/" + parsedUrl.path) { parsedUrl = value } } @@ -789,6 +792,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur var appName: String? var startApp: String? var text: String? + var profile: Bool = false if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { @@ -831,6 +835,8 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur startGroup = "" } else if queryItem.name == "startchannel" { startChannel = "" + } else if queryItem.name == "profile" { + profile = true } } } @@ -910,6 +916,13 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } convertedUrl = result } + if profile, let current = convertedUrl { + if current.contains("?") { + convertedUrl = current + "&profile" + } else { + convertedUrl = current + "?profile" + } + } } } else if parsedUrl.host == "hostOverride" { if let components = URLComponents(string: "/?" + query) { @@ -960,6 +973,20 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } } handleResolvedUrl(.premiumMultiGift(reference: reference)) + } else if parsedUrl.host == "stars_topup" { + var amount: Int64? + if let components = URLComponents(string: "/?" + query) { + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "amount" { + amount = Int64(value) + } + } + } + } + } + handleResolvedUrl(.starsTopup(amount: amount)) } else if parsedUrl.host == "addlist" { if let components = URLComponents(string: "/?" + query) { var slug: String? @@ -1050,14 +1077,13 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur return } + let urlScheme = (parsedUrl.scheme ?? "").lowercased() var isInternetUrl = false - if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { + if ["http", "https"].contains(urlScheme) { isInternetUrl = true } - if context.sharedContext.immediateExperimentalUISettings.browserExperiment { - if parsedUrl.scheme == "ipfs" || parsedUrl.scheme == "ipns" { - isInternetUrl = true - } + if urlScheme == "tonsite" { + isInternetUrl = true } if isInternetUrl { @@ -1076,26 +1102,53 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur settings = .defaultSettings } if accessChallengeData.data.isLockable { - if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == nil { - settings = WebBrowserSettings(defaultWebBrowser: "safari") + if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == "inApp" { + settings = WebBrowserSettings(defaultWebBrowser: "safari", exceptions: []) } } return settings } - var isCompact = false - if let metrics = navigationController?.validLayout?.metrics, case .compact = metrics.widthClass { - isCompact = true - } +// var isCompact = false +// if let metrics = navigationController?.validLayout?.metrics, case .compact = metrics.widthClass { +// isCompact = true +// } let _ = (settings |> deliverOnMainQueue).startStandalone(next: { settings in - if settings.defaultWebBrowser == nil { - if isCompact && context.sharedContext.immediateExperimentalUISettings.browserExperiment { + var isTonSite = false + if let host = parsedUrl.host, host.lowercased().hasSuffix(".ton") { + isTonSite = true + } else if let scheme = parsedUrl.scheme, scheme.lowercased().hasPrefix("tonsite") { + isTonSite = true + } + + if let defaultWebBrowser = settings.defaultWebBrowser, defaultWebBrowser != "inApp" && !isTonSite { + let openInOptions = availableOpenInOptions(context: context, item: .url(url: url)) + if let option = openInOptions.first(where: { $0.identifier == settings.defaultWebBrowser }) { + if case let .openUrl(openInUrl) = option.action() { + context.sharedContext.applicationBindings.openUrl(openInUrl) + } else { + context.sharedContext.applicationBindings.openUrl(url) + } + } else { + context.sharedContext.applicationBindings.openUrl(url) + } + } else { + var isExceptedDomain = false + let host = ".\((parsedUrl.host ?? "").lowercased())" + for exception in settings.exceptions { + if host.hasSuffix(".\(exception.domain)") { + isExceptedDomain = true + break + } + } + + if (settings.defaultWebBrowser == nil && !isExceptedDomain) || isTonSite { let controller = BrowserScreen(context: context, subject: .webPage(url: parsedUrl.absoluteString)) navigationController?.pushViewController(controller) } else { - if let window = navigationController?.view.window { + if let window = navigationController?.view.window, !isExceptedDomain { let controller = SFSafariViewController(url: parsedUrl) controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor @@ -1104,17 +1157,6 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur context.sharedContext.applicationBindings.openUrl(parsedUrl.absoluteString) } } - } else { - let openInOptions = availableOpenInOptions(context: context, item: .url(url: url)) - if let option = openInOptions.first(where: { $0.identifier == settings.defaultWebBrowser }) { - if case let .openUrl(url) = option.action() { - context.sharedContext.applicationBindings.openUrl(url) - } else { - context.sharedContext.applicationBindings.openUrl(url) - } - } else { - context.sharedContext.applicationBindings.openUrl(url) - } } }) } diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index e810e8711c8..c731e46943b 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -84,7 +84,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in - }, tapMessage: nil, clickThroughMessage: { + }, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in @@ -223,7 +223,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.isGlobalSearch = false } - self.historyNode = ChatHistoryListNodeImpl(context: context, updatedPresentationData: (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tag: .tag(tagMask), source: source, subject: .message(id: .id(initialMessageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch), isChatPreview: false, messageTransitionNode: { return nil }) + self.historyNode = ChatHistoryListNodeImpl(context: context, updatedPresentationData: (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tag: .tag(tagMask), source: source, subject: .message(id: .id(initialMessageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch), isChatPreview: false, messageTransitionNode: { return nil }) self.historyNode.clipsToBounds = true super.init() @@ -565,7 +565,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } let chatLocationContextHolder = Atomic(value: nil) - let historyNode = ChatHistoryListNodeImpl(context: self.context, updatedPresentationData: (self.context.sharedContext.currentPresentationData.with({ $0 }), self.context.sharedContext.presentationData), chatLocation: self.chatLocation, chatLocationContextHolder: chatLocationContextHolder, tag: .tag(tagMask), source: .default, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch), isChatPreview: false, messageTransitionNode: { return nil }) + let historyNode = ChatHistoryListNodeImpl(context: self.context, updatedPresentationData: (self.context.sharedContext.currentPresentationData.with({ $0 }), self.context.sharedContext.presentationData), chatLocation: self.chatLocation, chatLocationContextHolder: chatLocationContextHolder, tag: .tag(tagMask), source: .default, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch), isChatPreview: false, messageTransitionNode: { return nil }) historyNode.clipsToBounds = true historyNode.preloadPages = true historyNode.stackFromBottom = true diff --git a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift index a09f5b61dec..9b28b54b499 100644 --- a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift +++ b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift @@ -72,7 +72,7 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { } else { return SharedMediaPlaybackData(type: .music, source: source) } - case let .Video(_, _, flags, _): + case let .Video(_, _, flags, _, _): if flags.contains(.instantRoundVideo) { return SharedMediaPlaybackData(type: .instantVideo, source: source) } else { @@ -129,7 +129,7 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { displayData = SharedMediaPlaybackDisplayData.music(title: updatedTitle, performer: updatedPerformer, albumArt: albumArt, long: CGFloat(duration) > 10.0 * 60.0, caption: caption) } return displayData - case let .Video(_, _, flags, _): + case let .Video(_, _, flags, _, _): if flags.contains(.instantRoundVideo) { return SharedMediaPlaybackDisplayData.instantVideo(author: self.message.effectiveAuthor.flatMap(EnginePeer.init), peer: self.message.peers[self.message.id.peerId].flatMap(EnginePeer.init), timestamp: self.message.timestamp) } else { diff --git a/submodules/TelegramUI/Sources/PollResultsController.swift b/submodules/TelegramUI/Sources/PollResultsController.swift index 7b35b3fd0fc..071e7646384 100644 --- a/submodules/TelegramUI/Sources/PollResultsController.swift +++ b/submodules/TelegramUI/Sources/PollResultsController.swift @@ -333,7 +333,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, me displayCount = Int(voterCount) } for peerIndex in 0 ..< displayCount { - 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) + 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) let peer = EngineRenderedPeer(peer: EnginePeer(fakeUser)) entries.append(.optionPeer(optionId: i, index: peerIndex, peer: peer, optionText: optionTextHeader, optionTextEntities: optionTextHeaderEntities, optionAdditionalText: optionAdditionalTextHeader, optionCount: voterCount, optionExpanded: false, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: peerIndex % 2, isFirstInOption: peerIndex == 0)) } diff --git a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift index fa0fccb2a1b..a4d7aafa3f3 100644 --- a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift +++ b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift @@ -194,7 +194,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie index += 1 } } - case let .index(scrollSubject, position, directionHint, animated, highlight, displayLink): + case let .index(scrollSubject, position, directionHint, animated, highlight, displayLink, _): let scrollIndex = scrollSubject var position = position if case .center = position, highlight { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index b0cd8739876..aa271317b37 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -70,6 +70,7 @@ import StarsPurchaseScreen import StarsTransferScreen import StarsTransactionScreen import StarsWithdrawalScreen +import MiniAppListScreen import NGCore import NGData @@ -1832,7 +1833,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return presentAddMembersImpl(context: context, updatedPresentationData: updatedPresentationData, parentController: parentController, groupPeer: groupPeer, selectAddMemberDisposable: selectAddMemberDisposable, addMemberDisposable: addMemberDisposable) } - public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)? = nil, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem { + public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: ((UIView?, CGPoint?) -> Void)? = nil, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem { let controllerInteraction: ChatControllerInteraction controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in @@ -1842,8 +1843,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, navigateToThreadMessage: { _, _, _ in }, tapMessage: { message in tapMessage?(message) - }, clickThroughMessage: { - clickThroughMessage?() + }, clickThroughMessage: { view, location in + clickThroughMessage?(view, location) }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in @@ -1976,8 +1977,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { }) } - public func openChatInstantPage(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?, navigationController: NavigationController) { - openChatInstantPageImpl(context: context, message: message, sourcePeerType: sourcePeerType, navigationController: navigationController) + public func makeInstantPageController(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController? { + return makeInstantPageControllerImpl(context: context, message: message, sourcePeerType: sourcePeerType) + } + + public func makeInstantPageController(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController { + return makeInstantPageControllerImpl(context: context, webPage: webPage, anchor: anchor, sourceLocation: sourceLocation) } public func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) { @@ -2316,27 +2321,29 @@ public final class SharedAccountContextImpl: SharedAccountContext { return PremiumLimitScreen(context: context, subject: mappedSubject, count: count, forceDark: forceDark, cancel: cancel, action: action) } - public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (() -> Void)?) -> ViewController { - let options = Promise<[PremiumGiftCodeOption]>() - options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil)) - + public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let limit: Int32 = 10 var reachedLimitImpl: ((Int32) -> Void)? var presentBirthdayPickerImpl: (() -> Void)? let mode: ContactMultiselectionControllerMode + var starsMode: ContactSelectionControllerMode = .generic var currentBirthdays: [EnginePeer.Id: TelegramBirthday]? if case let .chatList(birthdays) = source, let birthdays, !birthdays.isEmpty { - mode = .premiumGifting(birthdays: birthdays, selectToday: true) + mode = .premiumGifting(birthdays: birthdays, selectToday: true, hasActions: true) currentBirthdays = birthdays } else if case let .settings(birthdays) = source, let birthdays, !birthdays.isEmpty { - mode = .premiumGifting(birthdays: birthdays, selectToday: false) + mode = .premiumGifting(birthdays: birthdays, selectToday: false, hasActions: true) + currentBirthdays = birthdays + } else if case let .stars(birthdays) = source { + mode = .premiumGifting(birthdays: birthdays, selectToday: false, hasActions: false) + starsMode = .starsGifting(birthdays: birthdays, hasActions: false) currentBirthdays = birthdays } else { - mode = .premiumGifting(birthdays: nil, selectToday: false) + mode = .premiumGifting(birthdays: nil, selectToday: false, hasActions: true) } - + let contactOptions: Signal<[ContactListAdditionalOption], NoError> if currentBirthdays != nil || "".isEmpty { contactOptions = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId)) @@ -2362,30 +2369,96 @@ public final class SharedAccountContextImpl: SharedAccountContext { var openProfileImpl: ((EnginePeer) -> Void)? var sendMessageImpl: ((EnginePeer) -> Void)? - let controller = context.sharedContext.makeContactMultiselectionController( - ContactMultiselectionControllerParams( + let controller: ViewController + if case .stars = source { + let options = Promise<[StarsGiftOption]>() + options.set(context.engine.payments.starsGiftOptions(peerId: nil)) + let contactsController = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( context: context, - mode: mode, - options: contactOptions, - isPeerEnabled: { peer in - if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) { - return true - } else { - return false - } - }, - limit: limit, - reachedLimit: { limit in - reachedLimitImpl?(limit) - }, - openProfile: { peer in - openProfileImpl?(peer) - }, - sendMessage: { peer in - sendMessageImpl?(peer) + mode: starsMode, + autoDismiss: false, + title: { strings in return strings.Stars_Purchase_GiftStars }, + options: contactOptions + )) + let _ = (contactsController.result + |> deliverOnMainQueue).start(next: { result in + if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer { + completion?([peer.id]) } + }) + controller = contactsController + } else { + let options = Promise<[PremiumGiftCodeOption]>() + options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil)) + let contactsController = context.sharedContext.makeContactMultiselectionController( + ContactMultiselectionControllerParams( + context: context, + mode: mode, + options: contactOptions, + isPeerEnabled: { peer in + if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) { + return true + } else { + return false + } + }, + limit: limit, + reachedLimit: { limit in + reachedLimitImpl?(limit) + }, + openProfile: { peer in + openProfileImpl?(peer) + }, + sendMessage: { peer in + sendMessageImpl?(peer) + } + ) ) - ) + let _ = combineLatest(queue: Queue.mainQueue(), contactsController.result, options.get()) + .startStandalone(next: { [weak contactsController] result, options in + guard let controller = contactsController else { + return + } + var peerIds: [PeerId] = [] + if case let .result(peerIdsValue, _) = result { + peerIds = peerIdsValue.compactMap({ peerId in + if case let .peer(peerId) = peerId { + return peerId + } else { + return nil + } + }) + } + guard !peerIds.isEmpty else { + return + } + + let mappedOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + var pushImpl: ((ViewController) -> Void)? + var filterImpl: (() -> Void)? + let giftController = PremiumGiftScreen(context: context, peerIds: peerIds, options: mappedOptions, source: source, pushController: { c in + pushImpl?(c) + }, completion: { + filterImpl?() + + if case .chatList = source, let _ = currentBirthdays { + let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .todayBirthdays).startStandalone() + } + }) + pushImpl = { [weak giftController] c in + giftController?.push(c) + } + filterImpl = { [weak giftController] in + if let navigationController = giftController?.navigationController as? NavigationController { + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is ContactMultiselectionController) && !($0 is PremiumGiftScreen) } + navigationController.setViewControllers(controllers, animated: true) + } + } + controller.push(giftController) + }) + controller = contactsController + } reachedLimitImpl = { [weak controller] limit in guard let controller else { @@ -2394,52 +2467,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { HapticFeedback().error() controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Premium_Gift_ContactSelection_MaximumReached("\(limit)").string, timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current) } - - let _ = combineLatest(queue: Queue.mainQueue(), controller.result, options.get()) - .startStandalone(next: { [weak controller] result, options in - guard let controller else { - return - } - var peerIds: [PeerId] = [] - if case let .result(peerIdsValue, _) = result { - peerIds = peerIdsValue.compactMap({ peerId in - if case let .peer(peerId) = peerId { - return peerId - } else { - return nil - } - }) - } - guard !peerIds.isEmpty else { - return - } - let mappedOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } - var pushImpl: ((ViewController) -> Void)? - var filterImpl: (() -> Void)? - let giftController = PremiumGiftScreen(context: context, peerIds: peerIds, options: mappedOptions, source: source, pushController: { c in - pushImpl?(c) - }, completion: { - filterImpl?() - completion?() - - if case .chatList = source, let _ = currentBirthdays { - let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .todayBirthdays).startStandalone() - } - }) - pushImpl = { [weak giftController] c in - giftController?.push(c) - } - filterImpl = { [weak giftController] in - if let navigationController = giftController?.navigationController as? NavigationController { - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is ContactMultiselectionController) && !($0 is PremiumGiftScreen) } - navigationController.setViewControllers(controllers, animated: true) - } - } - controller.push(giftController) - }) - sendMessageImpl = { [weak self, weak controller] peer in guard let self, let controller, let navigationController = controller.navigationController as? NavigationController else { return @@ -2646,6 +2674,53 @@ public final class SharedAccountContextImpl: SharedAccountContext { }) } + public func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, transitionOut: @escaping () -> BotPreviewEditorTransitionOut?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController { + let subject: Signal + if let asset = source as? PHAsset { + subject = .single(.asset(asset)) + } else if let image = source as? UIImage { + subject = .single(.image(image, PixelDimensions(image.size), nil, .bottomRight)) + } else { + subject = .single(.empty(PixelDimensions(width: 1080, height: 1920))) + } + let editorController = MediaEditorScreen( + context: context, + mode: .botPreview, + subject: subject, + customTarget: nil, + transitionIn: transitionArguments.flatMap { .gallery( + MediaEditorScreen.TransitionIn.GalleryTransitionIn( + sourceView: $0.0, + sourceRect: $0.1, + sourceImage: $0.2 + ) + ) }, + transitionOut: { finished, isNew in + if !finished, let transitionArguments { + return MediaEditorScreen.TransitionOut( + destinationView: transitionArguments.0, + destinationRect: transitionArguments.0.bounds, + destinationCornerRadius: 0.0 + ) + } else if finished, let transitionOut = transitionOut(), let destinationView = transitionOut.destinationView { + return MediaEditorScreen.TransitionOut( + destinationView: destinationView, + destinationRect: transitionOut.destinationRect, + destinationCornerRadius: transitionOut.destinationCornerRadius, + completion: transitionOut.completion + ) + } + return nil + }, completion: { result, commit in + completion(result, commit) + } as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void + ) + editorController.cancelled = { _ in + cancelled() + } + return editorController + } + public func makeStickerEditorScreen(context: AccountContext, source: Any?, intro: Bool, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController { let subject: Signal var mode: MediaEditorScreen.Mode.StickerEditorMode @@ -2713,13 +2788,42 @@ public final class SharedAccountContextImpl: SharedAccountContext { } return editorController } + + public func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: (url: String, name: String?)?, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController { + let subject: Signal + if let image = source as? UIImage { + subject = .single(.image(image, PixelDimensions(image.size), nil, .bottomRight)) + } else if let path = source as? String { + subject = .single(.video(path, nil, false, nil, nil, PixelDimensions(width: 1080, height: 1920), 0.0, [], .bottomRight)) + } else { + subject = .single(.empty(PixelDimensions(width: 1080, height: 1920))) + } + let editorController = MediaEditorScreen( + context: context, + mode: .storyEditor, + subject: subject, + customTarget: nil, + initialCaption: text.flatMap { NSAttributedString(string: $0) }, + initialLink: link, + transitionIn: nil, + transitionOut: { finished, isNew in + return nil + }, completion: { result, commit in + completion(result, commit) + } as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void + ) +// editorController.cancelled = { _ in +// cancelled() +// } + return editorController + } public func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController { return mediaPickerController(context: context, hasSearch: hasSearch, completion: completion) } - public func makeStoryMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController { - return storyMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed, groupsPresented: groupsPresented) + public func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController { + return storyMediaPickerController(context: context, isDark: isDark, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed, groupsPresented: groupsPresented) } public func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController { @@ -2761,10 +2865,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StarsTransactionsScreen(context: context, starsContext: starsContext) } - public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int64?, completion: @escaping (Int64) -> Void) -> ViewController { - return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: peerId, requiredStars: requiredStars, modal: true, completion: completion) + public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, completion: @escaping (Int64) -> Void) -> ViewController { + 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 { return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: source, extendedMedia: extendedMedia, inputData: inputData, completion: completion) } @@ -2788,6 +2892,22 @@ public final class SharedAccountContextImpl: SharedAccountContext { public func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController { return StarsWithdrawScreen(context: context, mode: .withdraw(stats), completion: completion) } + + public func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController { + return StarsTransactionScreen(context: context, subject: .gift(message), action: {}) + } + + public func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal { + return MiniAppListScreen.initialData(context: context) + } + + public func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController { + return MiniAppListScreen(context: context, initialData: initialData as! MiniAppListScreen.InitialData) + } + + public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) { + openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService) + } } private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 5827985b85c..f8d58fd3452 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -468,7 +468,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } @discardableResult - public func openStoryCamera(customTarget: EnginePeer.Id?, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? { + public func openStoryCamera(customTarget: Stories.PendingTarget?, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? { guard let controller = self.viewControllers.last as? ViewController else { return nil } @@ -506,7 +506,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return CameraScreen.TransitionOut( destinationView: destinationView, destinationRect: transitionOut.destinationRect, - destinationCornerRadius: transitionOut.destinationCornerRadius + destinationCornerRadius: transitionOut.destinationCornerRadius, + completion: transitionOut.completion ) } else { return nil @@ -554,24 +555,37 @@ public final class TelegramRootController: NavigationController, TelegramRootCon transitionIn = .camera } + let mediaEditorCustomTarget = customTarget.flatMap { value -> EnginePeer.Id? in + switch value { + case .myStories: + return nil + case let .peer(id): + return id + case let .botPreview(id, _): + return id + } + } + let controller = MediaEditorScreen( context: context, mode: .storyEditor, subject: subject, - customTarget: customTarget, + customTarget: mediaEditorCustomTarget, transitionIn: transitionIn, transitionOut: { finished, isNew in if finished, let transitionOut = (externalState.transitionOut ?? transitionOut)(externalState.storyTarget, false), let destinationView = transitionOut.destinationView { return MediaEditorScreen.TransitionOut( destinationView: destinationView, destinationRect: transitionOut.destinationRect, - destinationCornerRadius: transitionOut.destinationCornerRadius + destinationCornerRadius: transitionOut.destinationCornerRadius, + completion: transitionOut.completion ) } else if !finished, let resultTransition, let (destinationView, destinationRect) = resultTransition.transitionOut(isNew) { return MediaEditorScreen.TransitionOut( destinationView: destinationView, destinationRect: destinationRect, - destinationCornerRadius: 0.0 + destinationCornerRadius: 0.0, + completion: nil ) } else { return nil @@ -582,39 +596,47 @@ public final class TelegramRootController: NavigationController, TelegramRootCon commit({}) return } - - let target: Stories.PendingTarget - let targetPeerId: EnginePeer.Id - if let customTarget { - target = .peer(customTarget) - targetPeerId = customTarget - } else { - if let sendAsPeerId = result.options.sendAsPeerId { - target = .peer(sendAsPeerId) - targetPeerId = sendAsPeerId - } else { - target = .myStories - targetPeerId = context.account.peerId - } - } - externalState.storyTarget = target - - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId)) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in - guard let self, let peer else { - return - } - - if case let .user(user) = peer { - externalState.isPeerArchived = user.storiesHidden ?? false - } else if case let .channel(channel) = peer { - externalState.isPeerArchived = channel.storiesHidden ?? false - } - - self.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + + if let customTarget, case .botPreview = customTarget { + externalState.storyTarget = customTarget + self.proceedWithStoryUpload(target: customTarget, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) dismissCameraImpl?() - }) + return + } else { + let target: Stories.PendingTarget + let targetPeerId: EnginePeer.Id + if let customTarget, case let .peer(id) = customTarget { + target = .peer(id) + targetPeerId = id + } else { + if let sendAsPeerId = result.options.sendAsPeerId { + target = .peer(sendAsPeerId) + targetPeerId = sendAsPeerId + } else { + target = .myStories + targetPeerId = context.account.peerId + } + } + externalState.storyTarget = target + + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId)) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in + guard let self, let peer else { + return + } + + if case let .user(user) = peer { + externalState.isPeerArchived = user.storiesHidden ?? false + } else if case let .channel(channel) = peer { + externalState.isPeerArchived = channel.storiesHidden ?? false + } + + self.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + + dismissCameraImpl?() + }) + } } as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void ) controller.cancelled = { showDraftTooltip in @@ -675,12 +697,14 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return } let context = self.context - let targetPeerId: EnginePeer.Id + let targetPeerId: EnginePeer.Id? switch target { case let .peer(peerId): targetPeerId = peerId case .myStories: targetPeerId = context.account.peerId + case .botPreview: + targetPeerId = nil } if let rootTabController = self.rootTabController { @@ -762,7 +786,9 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return } - chatListController.scrollToStories(peerId: targetPeerId) + if let targetPeerId { + chatListController.scrollToStories(peerId: targetPeerId) + } Queue.mainQueue().justDispatch { commit({}) } @@ -816,7 +842,17 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return nil } } - media = .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers) + + var coverTime: Double? + if let coverImageTimestamp = values.coverImageTimestamp { + if let trimRange = values.videoTrimRange { + coverTime = min(duration, coverImageTimestamp - trimRange.lowerBound) + } else { + coverTime = min(duration, coverImageTimestamp) + } + } + + media = .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers, coverTime: coverTime) } default: break @@ -867,8 +903,22 @@ public final class TelegramRootController: NavigationController, TelegramRootCon //Xcode 16 #if canImport(ContactProvider) extension MediaEditorScreen.Result: @retroactive MediaEditorScreenResult { + public var target: Stories.PendingTarget { + if let sendAsPeerId = self.options.sendAsPeerId { + return .peer(sendAsPeerId) + } else { + return .myStories + } + } } #else extension MediaEditorScreen.Result: MediaEditorScreenResult { + public var target: Stories.PendingTarget { + if let sendAsPeerId = self.options.sendAsPeerId { + return .peer(sendAsPeerId) + } else { + return .myStories + } + } } #endif diff --git a/submodules/TelegramUI/Sources/TextLinkHandling.swift b/submodules/TelegramUI/Sources/TextLinkHandling.swift index 3dbfd4f567b..5c3d6a7f650 100644 --- a/submodules/TelegramUI/Sources/TextLinkHandling.swift +++ b/submodules/TelegramUI/Sources/TextLinkHandling.swift @@ -15,6 +15,7 @@ import JoinLinkPreviewUI import PresentationDataUtils import UrlWhitelist import UndoUI +import BrowserUI func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, navigateDisposable: MetaDisposable, controller: ViewController, action: TextLinkItemActionType, itemLink: TextLinkItem) { let presentImpl: (ViewController, Any?) -> Void = { controllerToPresent, _ in @@ -72,7 +73,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n openResolvedPeerImpl(EnginePeer(peer), .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive))) case let .channelMessage(peer, messageId, timecode): if let navigationController = controller.navigationController as? NavigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode))) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode, setupReply: false))) } case let .replyThreadMessage(replyThreadMessage, messageId): if let navigationController = controller.navigationController as? NavigationController, let effectiveMessageId = replyThreadMessage.effectiveMessageId { @@ -87,8 +88,10 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n case let .stickerPack(name, _): let packReference: StickerPackReference = .name(name) controller.present(StickerPackScreen(context: context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root)) - case let .instantView(webpage, anchor): - (controller.navigationController as? NavigationController)?.pushViewController(InstantPageController(context: context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: peerId.flatMap(MediaResourceUserLocation.peer) ?? .other, peerType: .group), anchor: anchor)) + case let .instantView(webPage, anchor): + 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)) diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index ac5d20494ff..ae7d9b6ea00 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -38,7 +38,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var enableVoipTcp: Bool public var experimentalCompatibility: Bool public var enableDebugDataDisplay: Bool - public var acceleratedStickers: Bool + public var rippleEffect: Bool public var inlineStickers: Bool public var localTranscription: Bool public var enableReactionOverrides: Bool @@ -75,7 +75,7 @@ public struct ExperimentalUISettings: Codable, Equatable { enableVoipTcp: false, experimentalCompatibility: false, enableDebugDataDisplay: false, - acceleratedStickers: false, + rippleEffect: false, inlineStickers: false, localTranscription: false, enableReactionOverrides: false, @@ -112,7 +112,7 @@ public struct ExperimentalUISettings: Codable, Equatable { enableVoipTcp: Bool, experimentalCompatibility: Bool, enableDebugDataDisplay: Bool, - acceleratedStickers: Bool, + rippleEffect: Bool, inlineStickers: Bool, localTranscription: Bool, enableReactionOverrides: Bool, @@ -146,7 +146,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enableVoipTcp = enableVoipTcp self.experimentalCompatibility = experimentalCompatibility self.enableDebugDataDisplay = enableDebugDataDisplay - self.acceleratedStickers = acceleratedStickers + self.rippleEffect = rippleEffect self.inlineStickers = inlineStickers self.localTranscription = localTranscription self.enableReactionOverrides = enableReactionOverrides @@ -184,7 +184,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enableVoipTcp = (try container.decodeIfPresent(Int32.self, forKey: "enableVoipTcp") ?? 0) != 0 self.experimentalCompatibility = (try container.decodeIfPresent(Int32.self, forKey: "experimentalCompatibility") ?? 0) != 0 self.enableDebugDataDisplay = (try container.decodeIfPresent(Int32.self, forKey: "enableDebugDataDisplay") ?? 0) != 0 - self.acceleratedStickers = (try container.decodeIfPresent(Int32.self, forKey: "acceleratedStickers") ?? 0) != 0 + self.rippleEffect = (try container.decodeIfPresent(Int32.self, forKey: "rippleEffect") ?? 0) != 0 self.inlineStickers = (try container.decodeIfPresent(Int32.self, forKey: "inlineStickers") ?? 0) != 0 self.localTranscription = (try container.decodeIfPresent(Int32.self, forKey: "localTranscription") ?? 0) != 0 self.enableReactionOverrides = try container.decodeIfPresent(Bool.self, forKey: "enableReactionOverrides") ?? false @@ -222,7 +222,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode((self.enableVoipTcp ? 1 : 0) as Int32, forKey: "enableVoipTcp") try container.encode((self.experimentalCompatibility ? 1 : 0) as Int32, forKey: "experimentalCompatibility") try container.encode((self.enableDebugDataDisplay ? 1 : 0) as Int32, forKey: "enableDebugDataDisplay") - try container.encode((self.acceleratedStickers ? 1 : 0) as Int32, forKey: "acceleratedStickers") + try container.encode((self.rippleEffect ? 1 : 0) as Int32, forKey: "rippleEffect") try container.encode((self.inlineStickers ? 1 : 0) as Int32, forKey: "inlineStickers") try container.encode((self.localTranscription ? 1 : 0) as Int32, forKey: "localTranscription") try container.encode(self.enableReactionOverrides, forKey: "enableReactionOverrides") diff --git a/submodules/TelegramUIPreferences/Sources/InstantPagePresentationSettings.swift b/submodules/TelegramUIPreferences/Sources/InstantPagePresentationSettings.swift index 26a1ddba44a..420dca7e112 100644 --- a/submodules/TelegramUIPreferences/Sources/InstantPagePresentationSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/InstantPagePresentationSettings.swift @@ -11,6 +11,8 @@ public enum InstantPageThemeType: Int32 { } public enum InstantPagePresentationFontSize: Int32 { + case xxsmall = -2 + case xsmall = -1 case small = 0 case standard = 1 case large = 2 diff --git a/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift b/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift index 74c5eced057..5e2beb637c2 100644 --- a/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift @@ -491,6 +491,16 @@ public enum MediaAutoDownloadPeerType { case channel } +public struct InstantPageSourceLocation { + public var userLocation: MediaResourceUserLocation + public var peerType: MediaAutoDownloadPeerType + + public init(userLocation: MediaResourceUserLocation, peerType: MediaAutoDownloadPeerType) { + self.userLocation = userLocation + self.peerType = peerType + } +} + public func effectiveAutodownloadCategories(settings: MediaAutoDownloadSettings, networkType: MediaAutoDownloadNetworkType) -> MediaAutoDownloadCategories { let connection = settings.connectionSettings(for: networkType) switch connection.preset { diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index cba2c06b8c0..ebefd6a4f95 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -104,6 +104,7 @@ private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 { case storyDrafts = 4 case storySources = 5 case hashtagSearchRecentQueries = 6 + case browserRecentlyVisited = 7 } public struct ApplicationSpecificOrderedItemListCollectionId { @@ -114,4 +115,5 @@ public struct ApplicationSpecificOrderedItemListCollectionId { public static let storyDrafts = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.storyDrafts.rawValue) public static let storySources = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.storySources.rawValue) public static let hashtagSearchRecentQueries = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.hashtagSearchRecentQueries.rawValue) + public static let browserRecentlyVisited = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.browserRecentlyVisited.rawValue) } diff --git a/submodules/TelegramUIPreferences/Sources/WebBrowserSettings.swift b/submodules/TelegramUIPreferences/Sources/WebBrowserSettings.swift index 0739d93e818..68a67601836 100644 --- a/submodules/TelegramUIPreferences/Sources/WebBrowserSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/WebBrowserSettings.swift @@ -3,35 +3,81 @@ import Postbox import TelegramCore import SwiftSignalKit +public struct WebBrowserException: Codable, Equatable { + public let domain: String + public let title: String + public let icon: TelegramMediaImage? + + public init(domain: String, title: String, icon: TelegramMediaImage?) { + self.domain = domain + self.title = title + self.icon = icon + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.domain = try container.decode(String.self, forKey: "domain") + self.title = try container.decode(String.self, forKey: "title") + self.icon = try container.decodeIfPresent(TelegramMediaImage.self, forKey: "icon") + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.domain, forKey: "domain") + try container.encode(self.title, forKey: "title") + if let icon = self.icon { + try container.encode(icon, forKey: "icon") + } else { + try container.encodeNil(forKey: "icon") + } + } +} + public struct WebBrowserSettings: Codable, Equatable { public let defaultWebBrowser: String? + public let exceptions: [WebBrowserException] public static var defaultSettings: WebBrowserSettings { - return WebBrowserSettings(defaultWebBrowser: nil) + return WebBrowserSettings(defaultWebBrowser: nil, exceptions: []) } - public init(defaultWebBrowser: String?) { + public init(defaultWebBrowser: String?, exceptions: [WebBrowserException]) { self.defaultWebBrowser = defaultWebBrowser + self.exceptions = exceptions } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) self.defaultWebBrowser = try? container.decodeIfPresent(String.self, forKey: "defaultWebBrowser") + self.exceptions = (try? container.decodeIfPresent([WebBrowserException].self, forKey: "exceptions")) ?? [] } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringCodingKey.self) try container.encodeIfPresent(self.defaultWebBrowser, forKey: "defaultWebBrowser") + try container.encode(self.exceptions, forKey: "exceptions") } public static func ==(lhs: WebBrowserSettings, rhs: WebBrowserSettings) -> Bool { - return lhs.defaultWebBrowser == rhs.defaultWebBrowser + if lhs.defaultWebBrowser != rhs.defaultWebBrowser { + return false + } + if lhs.exceptions != rhs.exceptions { + return false + } + return true } public func withUpdatedDefaultWebBrowser(_ defaultWebBrowser: String?) -> WebBrowserSettings { - return WebBrowserSettings(defaultWebBrowser: defaultWebBrowser) + return WebBrowserSettings(defaultWebBrowser: defaultWebBrowser, exceptions: self.exceptions) + } + + public func withUpdatedExceptions(_ exceptions: [WebBrowserException]) -> WebBrowserSettings { + return WebBrowserSettings(defaultWebBrowser: self.defaultWebBrowser, exceptions: exceptions) } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index 905d87206b0..3df84b67294 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -251,6 +251,10 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent } if displayImage { + if captureProtected { + setLayerDisableScreenshots(self.imageNode.layer, captureProtected) + } + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: fileReference, imageReference: imageReference, onlyFullSize: onlyFullSizeThumbnail, useLargeThumbnail: useLargeThumbnail, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail || fileReference.media.isInstantVideo) |> map { [weak self] getSize, getData in Queue.mainQueue().async { if let strongSelf = self, strongSelf.dimensions == nil { diff --git a/submodules/TelegramVoip/BUILD b/submodules/TelegramVoip/BUILD index f60bbab91f5..9de5d24e9ab 100644 --- a/submodules/TelegramVoip/BUILD +++ b/submodules/TelegramVoip/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/TgVoip:TgVoip", "//submodules/TgVoipWebrtc:TgVoipWebrtc", + "//submodules/FFMpegBinding", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramVoip/Package.swift b/submodules/TelegramVoip/Package.swift index 47993729386..4ecef069ce0 100644 --- a/submodules/TelegramVoip/Package.swift +++ b/submodules/TelegramVoip/Package.swift @@ -17,6 +17,7 @@ let package = Package( // .package(url: /* package url */, from: "1.0.0"), .package(name: "TgVoipWebrtc", path: "../../../tgcalls"), .package(name: "SSignalKit", path: "../SSignalKit"), + .package(name: "FFMpegBinding", path: "../FFMpegBinding"), .package(name: "TelegramCore", path: "../TelegramCore") ], @@ -28,6 +29,7 @@ let package = Package( dependencies: [ .product(name: "TgVoipWebrtc", package: "TgVoipWebrtc", condition: nil), .product(name: "SwiftSignalKit", package: "SSignalKit", condition: nil), + .product(name: "FFMpegBinding", package: "FFMpegBinding", condition: nil), .product(name: "TelegramCore", package: "TelegramCore", condition: nil), ], path: "Sources", diff --git a/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift b/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift index 3ae1b27e2e3..115ee7edb84 100644 --- a/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift +++ b/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift @@ -2,7 +2,12 @@ import Foundation import SwiftSignalKit import TgVoipWebrtc import TelegramCore +import Network +import Postbox +import FFMpegBinding + +@available(iOS 12.0, macOS 14.0, *) public final class WrappedMediaStreamingContext { private final class Impl { let queue: Queue @@ -132,3 +137,460 @@ public final class WrappedMediaStreamingContext { } } } +@available(iOS 12.0, macOS 14.0, *) +public final class ExternalMediaStreamingContext { + private final class Impl { + let queue: Queue + + private var broadcastPartsSource: BroadcastPartSource? + + private let resetPlaylistDisposable = MetaDisposable() + private let updatePlaylistDisposable = MetaDisposable() + + let masterPlaylistData = Promise() + let playlistData = Promise() + let mediumPlaylistData = Promise() + + init(queue: Queue, rejoinNeeded: @escaping () -> Void) { + self.queue = queue + } + + deinit { + self.updatePlaylistDisposable.dispose() + } + + func setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData?) { + if let audioStreamData { + let broadcastPartsSource = NetworkBroadcastPartSource(queue: self.queue, engine: audioStreamData.engine, callId: audioStreamData.callId, accessHash: audioStreamData.accessHash, isExternalStream: audioStreamData.isExternalStream) + self.broadcastPartsSource = broadcastPartsSource + + self.updatePlaylistDisposable.set(nil) + + let queue = self.queue + self.resetPlaylistDisposable.set(broadcastPartsSource.requestTime(completion: { [weak self] timestamp in + queue.async { + guard let self else { + return + } + + let segmentDuration: Int64 = 1000 + + var adjustedTimestamp: Int64 = 0 + if timestamp > 0 { + adjustedTimestamp = timestamp / segmentDuration * segmentDuration - 4 * segmentDuration + } + + if adjustedTimestamp > 0 { + var masterPlaylistData = "#EXTM3U\n" + + "#EXT-X-VERSION:6\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=3300000,RESOLUTION=1280x720,CODECS=\"avc1.64001f,mp4a.40.2\"\n" + + "hls_level_0.m3u8\n" + + masterPlaylistData += "#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=640x360,CODECS=\"avc1.64001f,mp4a.40.2\"\n" + + "hls_level_1.m3u8\n" + + self.masterPlaylistData.set(.single(masterPlaylistData)) + + self.beginUpdatingPlaylist(initialHeadTimestamp: adjustedTimestamp) + } + } + })) + } + } + + private func beginUpdatingPlaylist(initialHeadTimestamp: Int64) { + let segmentDuration: Int64 = 1000 + + var timestamp = initialHeadTimestamp + self.updatePlaylist(headTimestamp: timestamp, quality: 0) + self.updatePlaylist(headTimestamp: timestamp, quality: 1) + + self.updatePlaylistDisposable.set(( + Signal.single(Void()) + |> delay(1.0, queue: self.queue) + |> restart + |> deliverOn(self.queue) + ).start(next: { [weak self] _ in + guard let self else { + return + } + + timestamp += segmentDuration + self.updatePlaylist(headTimestamp: timestamp, quality: 0) + self.updatePlaylist(headTimestamp: timestamp, quality: 1) + })) + } + + private func updatePlaylist(headTimestamp: Int64, quality: Int) { + let segmentDuration: Int64 = 1000 + let headIndex = headTimestamp / segmentDuration + let minIndex = headIndex - 20 + + var playlistData = "#EXTM3U\n" + + "#EXT-X-VERSION:6\n" + + "#EXT-X-TARGETDURATION:1\n" + + "#EXT-X-MEDIA-SEQUENCE:\(minIndex)\n" + + "#EXT-X-INDEPENDENT-SEGMENTS\n" + + for index in minIndex ... headIndex { + playlistData.append("#EXTINF:1.000000,\n") + playlistData.append("hls_stream\(quality)_\(index).ts\n") + } + + //print("Player: updating playlist \(quality) \(minIndex) ... \(headIndex)") + + if quality == 0 { + self.playlistData.set(.single(playlistData)) + } else { + self.mediumPlaylistData.set(.single(playlistData)) + } + } + + func partData(index: Int, quality: Int) -> Signal { + let segmentDuration: Int64 = 1000 + let timestamp = Int64(index) * segmentDuration + + print("Player: request part(q: \(quality)) \(index) -> \(timestamp)") + + guard let broadcastPartsSource = self.broadcastPartsSource else { + return .single(nil) + } + + return Signal { subscriber in + return broadcastPartsSource.requestPart( + timestampMilliseconds: timestamp, + durationMilliseconds: segmentDuration, + subject: .video(channelId: 1, quality: quality == 0 ? .full : .medium), + completion: { part in + var data = part.oggData + if data.count > 32 { + data = data.subdata(in: 32 ..< data.count) + } + subscriber.putNext(data) + }, + rejoinNeeded: { + //TODO + } + ) + } + } + } + + private let queue = Queue() + let id: CallSessionInternalId + private let impl: QueueLocalObject + private var hlsServerDisposable: Disposable? + + public init(id: CallSessionInternalId, rejoinNeeded: @escaping () -> Void) { + self.id = id + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, rejoinNeeded: rejoinNeeded) + }) + + self.hlsServerDisposable = SharedHLSServer.shared.registerPlayer(streamingContext: self) + } + + deinit { + self.hlsServerDisposable?.dispose() + } + + public func setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData?) { + self.impl.with { impl in + impl.setAudioStreamData(audioStreamData: audioStreamData) + } + } + + public func masterPlaylistData() -> Signal { + return self.impl.signalWith { impl, subscriber in + impl.masterPlaylistData.get().start(next: subscriber.putNext) + } + } + + public func playlistData(quality: Int) -> Signal { + return self.impl.signalWith { impl, subscriber in + if quality == 0 { + impl.playlistData.get().start(next: subscriber.putNext) + } else { + impl.mediumPlaylistData.get().start(next: subscriber.putNext) + } + } + } + + public func partData(index: Int, quality: Int) -> Signal { + return self.impl.signalWith { impl, subscriber in + impl.partData(index: index, quality: quality).start(next: subscriber.putNext) + } + } +} +@available(iOS 12.0, macOS 14.0, *) +public final class SharedHLSServer { + public static let shared: SharedHLSServer = { + return SharedHLSServer() + }() + + private enum ResponseError { + case badRequest + case notFound + case internalServerError + + var httpString: String { + switch self { + case .badRequest: + return "400 Bad Request" + case .notFound: + return "404 Not Found" + case .internalServerError: + return "500 Internal Server Error" + } + } + } + + private final class ContextReference { + weak var streamingContext: ExternalMediaStreamingContext? + + init(streamingContext: ExternalMediaStreamingContext) { + self.streamingContext = streamingContext + } + } + @available(iOS 12.0, macOS 14.0, *) + private final class Impl { + private let queue: Queue + + private let port: NWEndpoint.Port + private var listener: NWListener? + + private var contextReferences = Bag() + + init(queue: Queue, port: UInt16) { + self.queue = queue + self.port = NWEndpoint.Port(rawValue: port)! + self.start() + } + + func start() { + let listener: NWListener + do { + listener = try NWListener(using: .tcp, on: self.port) + } catch { + Logger.shared.log("SharedHLSServer", "Failed to create listener: \(error)") + return + } + self.listener = listener + + listener.newConnectionHandler = { [weak self] connection in + guard let self else { + return + } + self.handleConnection(connection: connection) + } + + listener.stateUpdateHandler = { [weak self] state in + guard let self else { + return + } + switch state { + case .ready: + Logger.shared.log("SharedHLSServer", "Server is ready on port \(self.port)") + case let .failed(error): + Logger.shared.log("SharedHLSServer", "Server failed with error: \(error)") + self.listener?.cancel() + + self.listener?.start(queue: self.queue.queue) + default: + break + } + } + + listener.start(queue: self.queue.queue) + } + + private func handleConnection(connection: NWConnection) { + connection.start(queue: self.queue.queue) + connection.receive(minimumIncompleteLength: 1, maximumLength: 1024, completion: { [weak self] data, _, isComplete, error in + guard let self else { + return + } + if let data, !data.isEmpty { + self.handleRequest(data: data, connection: connection) + } else if isComplete { + connection.cancel() + } else if let error = error { + Logger.shared.log("SharedHLSServer", "Error on connection: \(error)") + connection.cancel() + } + }) + } + + private func handleRequest(data: Data, connection: NWConnection) { + guard let requestString = String(data: data, encoding: .utf8) else { + connection.cancel() + return + } + + if !requestString.hasPrefix("GET /") { + self.sendErrorAndClose(connection: connection) + return + } + guard let firstCrLf = requestString.range(of: "\r\n") else { + self.sendErrorAndClose(connection: connection) + return + } + let firstLine = String(requestString[requestString.index(requestString.startIndex, offsetBy: "GET /".count) ..< firstCrLf.lowerBound]) + if !(firstLine.hasSuffix(" HTTP/1.0") || firstLine.hasSuffix(" HTTP/1.1")) { + self.sendErrorAndClose(connection: connection) + return + } + + let requestPath = String(firstLine[firstLine.startIndex ..< firstLine.index(firstLine.endIndex, offsetBy: -" HTTP/1.1".count)]) + + guard let firstSlash = requestPath.range(of: "/") else { + self.sendErrorAndClose(connection: connection, error: .notFound) + return + } + guard let streamId = UUID(uuidString: String(requestPath[requestPath.startIndex ..< firstSlash.lowerBound])) else { + self.sendErrorAndClose(connection: connection) + return + } + guard let streamingContext = self.contextReferences.copyItems().first(where: { $0.streamingContext?.id == streamId })?.streamingContext else { + self.sendErrorAndClose(connection: connection) + return + } + + let filePath = String(requestPath[firstSlash.upperBound...]) + if filePath == "master.m3u8" { + let _ = (streamingContext.masterPlaylistData() + |> deliverOn(self.queue) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + self.sendResponseAndClose(connection: connection, data: result.data(using: .utf8)!) + }) + } else if filePath.hasPrefix("hls_level_") && filePath.hasSuffix(".m3u8") { + guard let levelIndex = Int(String(filePath[filePath.index(filePath.startIndex, offsetBy: "hls_level_".count) ..< filePath.index(filePath.endIndex, offsetBy: -".m3u8".count)])) else { + self.sendErrorAndClose(connection: connection) + return + } + + let _ = (streamingContext.playlistData(quality: levelIndex) + |> deliverOn(self.queue) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + self.sendResponseAndClose(connection: connection, data: result.data(using: .utf8)!) + }) + } else if filePath.hasPrefix("hls_stream") && filePath.hasSuffix(".ts") { + let fileId = String(filePath[filePath.index(filePath.startIndex, offsetBy: "hls_stream".count) ..< filePath.index(filePath.endIndex, offsetBy: -".ts".count)]) + guard let underscoreRange = fileId.range(of: "_") else { + self.sendErrorAndClose(connection: connection) + return + } + guard let levelIndex = Int(String(fileId[fileId.startIndex ..< underscoreRange.lowerBound])) else { + self.sendErrorAndClose(connection: connection) + return + } + guard let partIndex = Int(String(fileId[underscoreRange.upperBound...])) else { + self.sendErrorAndClose(connection: connection) + return + } + let _ = (streamingContext.partData(index: partIndex, quality: levelIndex) + |> deliverOn(self.queue) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + if let result { + let sourceTempFile = TempBox.shared.tempFile(fileName: "part.mp4") + let tempFile = TempBox.shared.tempFile(fileName: "part.ts") + defer { + TempBox.shared.dispose(sourceTempFile) + TempBox.shared.dispose(tempFile) + } + + guard let _ = try? result.write(to: URL(fileURLWithPath: sourceTempFile.path)) else { + self.sendErrorAndClose(connection: connection, error: .internalServerError) + return + } + + let sourcePath = sourceTempFile.path + FFMpegLiveMuxer.remux(sourcePath, to: tempFile.path, offsetSeconds: Double(partIndex)) + + if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path)) { + self.sendResponseAndClose(connection: connection, data: data) + } else { + self.sendErrorAndClose(connection: connection, error: .internalServerError) + } + } else { + self.sendErrorAndClose(connection: connection, error: .notFound) + } + }) + } else { + self.sendErrorAndClose(connection: connection, error: .notFound) + } + } + + private func sendErrorAndClose(connection: NWConnection, error: ResponseError = .badRequest) { + let errorResponse = "HTTP/1.1 \(error.httpString)\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n" + connection.send(content: errorResponse.data(using: .utf8), completion: .contentProcessed { error in + if let error { + Logger.shared.log("SharedHLSServer", "Failed to send response: \(error)") + } + connection.cancel() + }) + } + + private func sendResponseAndClose(connection: NWConnection, data: Data) { + let responseHeaders = "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nConnection: close\r\n\r\n" + var responseData = Data() + responseData.append(responseHeaders.data(using: .utf8)!) + responseData.append(data) + connection.send(content: responseData, completion: .contentProcessed { error in + if let error { + Logger.shared.log("SharedHLSServer", "Failed to send response: \(error)") + } + connection.cancel() + }) + } + + func registerPlayer(streamingContext: ExternalMediaStreamingContext) -> Disposable { + let queue = self.queue + let index = self.contextReferences.add(ContextReference(streamingContext: streamingContext)) + + return ActionDisposable { [weak self] in + queue.async { + guard let self else { + return + } + self.contextReferences.remove(index) + } + } + } + } + + private static let queue = Queue(name: "SharedHLSServer") + public let port: UInt16 = 8016 + private let impl: QueueLocalObject + + private init() { + let queue = SharedHLSServer.queue + let port = self.port + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, port: port) + }) + } + + fileprivate func registerPlayer(streamingContext: ExternalMediaStreamingContext) -> Disposable { + let disposable = MetaDisposable() + + self.impl.with { impl in + disposable.set(impl.registerPlayer(streamingContext: streamingContext)) + } + + return disposable + } +} diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 67f47dff20a..45b3e874faa 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -408,6 +408,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { case topic(id: Int64, info: EngineMessageHistoryThread.Info) case nameColors([UInt32]) case stars(tinted: Bool) + case ton } public let interactivelySelectedFromPackId: ItemCollectionId? diff --git a/submodules/TranslateUI/Sources/LocalizationListItem.swift b/submodules/TranslateUI/Sources/LocalizationListItem.swift index 1ee42bf8e3d..6723f2bdc54 100644 --- a/submodules/TranslateUI/Sources/LocalizationListItem.swift +++ b/submodules/TranslateUI/Sources/LocalizationListItem.swift @@ -37,10 +37,10 @@ public class LocalizationListItem: ListViewItem, ItemListItem { public let sectionId: ItemListSectionId let alwaysPlain: Bool let action: () -> Void - let setItemWithRevealedOptions: (String?, String?) -> Void - let removeItem: (String) -> Void + let setItemWithRevealedOptions: ((String?, String?) -> Void)? + let removeItem: ((String) -> Void)? - public init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, loading: Bool, editing: LocalizationListItemEditing, enabled: Bool = true, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) { + public init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, loading: Bool, editing: LocalizationListItemEditing, enabled: Bool = true, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: ((String?, String?) -> Void)?, removeItem: ((String) -> Void)?) { self.presentationData = presentationData self.id = id self.title = title @@ -138,7 +138,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { if self.editableControlNode != nil { return false } - if let _ = self.layoutParams?.0, let item = self.item, !item.loading { + if let _ = self.layoutParams?.0, let item = self.item, !item.loading, item.enabled { return super.canBeSelected } else { return false @@ -334,6 +334,9 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + leftInset, y: 8.0), size: titleLayout.size)) transition.updateFrame(node: strongSelf.subtitleNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + leftInset, y: strongSelf.titleNode.frame.maxY + 1.0), size: subtitleLayout.size)) + strongSelf.titleNode.alpha = item.enabled ? 1.0 : 0.5 + strongSelf.subtitleNode.alpha = item.enabled ? 1.0 : 0.5 + if let editableControlSizeAndApply = editableControlSizeAndApply { let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height)) if strongSelf.editableControlNode == nil { @@ -368,7 +371,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - if item.editing.editable { + if item.editing.editable, item.removeItem != nil { strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)])) } else { strongSelf.setRevealOptions((left: [], right: [])) @@ -491,13 +494,13 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { override func revealOptionsInteractivelyOpened() { if let item = self.item { - item.setItemWithRevealedOptions(item.id, nil) + item.setItemWithRevealedOptions?(item.id, nil) } } override func revealOptionsInteractivelyClosed() { if let item = self.item { - item.setItemWithRevealedOptions(nil, item.id) + item.setItemWithRevealedOptions?(nil, item.id) } } @@ -506,7 +509,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { self.revealOptionsInteractivelyClosed() if let item = self.item { - item.removeItem(item.id) + item.removeItem?(item.id) } } } diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 0d68fcff210..d9398de8a43 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -437,7 +437,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.visibility = true } - //TODO:localize self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) displayUndo = false @@ -713,12 +712,14 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold: MarkdownAttributeSet + var link = body if savedMessages { bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0), additionalAttributes: ["URL": ""]) + link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) } else { bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) } - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) self.textNode.attributedText = attributedText self.textNode.maximumNumberOfLines = 2 diff --git a/submodules/UrlEscaping/Sources/Punycode.swift b/submodules/UrlEscaping/Sources/Punycode.swift new file mode 100644 index 00000000000..2edbbb22e3e --- /dev/null +++ b/submodules/UrlEscaping/Sources/Punycode.swift @@ -0,0 +1,317 @@ +// +// Created by kojirof on 2018-11-19. +// Copyright (c) 2018 Gumob. All rights reserved. +// + +//MIT License +// +//Copyright (c) 2018 Gumob +// +//Permission is hereby granted, free of charge, to any person obtaining a copy +//of this software and associated documentation files (the "Software"), to deal +//in the Software without restriction, including without limitation the rights +//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: +// +//The above copyright notice and this permission notice shall be included in all +//copies or substantial portions of the Software. +// +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +//SOFTWARE. + +import Foundation + +public class Punycode { + + /// Punycode RFC 3492 + /// See https://www.ietf.org/rfc/rfc3492.txt for standard details + + private let base: Int = 36 + private let tMin: Int = 1 + private let tMax: Int = 26 + private let skew: Int = 38 + private let damp: Int = 700 + private let initialBias: Int = 72 + private let initialN: Int = 128 + + /// RFC 3492 specific + private let delimiter: Character = "-" + private let lowercase: ClosedRange = "a"..."z" + private let digits: ClosedRange = "0"..."9" + private let lettersBase: UInt32 = Character("a").unicodeScalars.first!.value + private let digitsBase: UInt32 = Character("0").unicodeScalars.first!.value + + /// IDNA + private let ace: String = "xn--" + + private func adaptBias(_ delta: Int, _ numberOfPoints: Int, _ firstTime: Bool) -> Int { + var delta: Int = delta + if firstTime { + delta /= damp + } else { + delta /= 2 + } + delta += delta / numberOfPoints + var k: Int = 0 + while delta > ((base - tMin) * tMax) / 2 { + delta /= base - tMin + k += base + } + return k + ((base - tMin + 1) * delta) / (delta + skew) + } + + /// Maps a punycode character to index + private func punycodeIndex(for character: Character) -> Int? { + if lowercase.contains(character) { + return Int(character.unicodeScalars.first!.value - lettersBase) + } else if digits.contains(character) { + return Int(character.unicodeScalars.first!.value - digitsBase) + 26 /// count of lowercase letters range + } else { + return nil + } + } + + /// Maps an index to corresponding punycode character + private func punycodeValue(for digit: Int) -> Character? { + guard digit < base else { return nil } + if digit < 26 { + return Character(UnicodeScalar(lettersBase.advanced(by: digit))!) + } else { + return Character(UnicodeScalar(digitsBase.advanced(by: digit - 26))!) + } + } + + /// Decodes punycode encoded string to original representation + /// + /// - Parameter punycode: Punycode encoding (RFC 3492) + /// - Returns: Decoded string or nil if the input cannot be decoded + public func decodePunycode(_ punycode: Substring) -> String? { + var n: Int = initialN + var i: Int = 0 + var bias: Int = initialBias + var output: [Character] = [] + var inputPosition = punycode.startIndex + + let delimiterPosition: Substring.Index = punycode.lastIndex(of: delimiter) ?? punycode.startIndex + if delimiterPosition > punycode.startIndex { + output.append(contentsOf: punycode[..= bias + tMax ? tMax : k - bias) + if digit < t { + break + } + w *= base - t + k += base + } while !punycodeInput.isEmpty + bias = adaptBias(i - oldI, output.count + 1, oldI == 0) + n += i / (output.count + 1) + i %= (output.count + 1) + guard n >= 0x80, let scalar = UnicodeScalar(n) else { + return nil + } + output.insert(Character(scalar), at: i) + i += 1 + } + + return String(output) + } + + /// Encodes string to punycode (RFC 3492) + /// + /// - Parameter input: Input string + /// - Returns: Punycode encoded string + public func encodePunycode(_ input: Substring) -> String? { + var n: Int = initialN + var delta: Int = 0 + var bias: Int = initialBias + var output: String = "" + for scalar in input.unicodeScalars { + if scalar.isASCII { + let char = Character(scalar) + output.append(char) + } else if !scalar.isValid { + return nil /// Encountered a scalar out of acceptable range + } + } + var handled: Int = output.count + let basic: Int = handled + if basic > 0 { + output.append(delimiter) + } + while handled < input.unicodeScalars.count { + var minimumCodepoint: Int = 0x10FFFF + for scalar: Unicode.Scalar in input.unicodeScalars { + if scalar.value < minimumCodepoint && scalar.value >= n { + minimumCodepoint = Int(scalar.value) + } + } + delta += (minimumCodepoint - n) * (handled + 1) + n = minimumCodepoint + for scalar: Unicode.Scalar in input.unicodeScalars { + if scalar.value < n { + delta += 1 + } else if scalar.value == n { + var q: Int = delta + var k: Int = base + while true { + let t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias) + if q < t { + break + } + guard let character: Character = punycodeValue(for: t + ((q - t) % (base - t))) else { return nil } + output.append(character) + q = (q - t) / (base - t) + k += base + } + guard let character: Character = punycodeValue(for: q) else { return nil } + output.append(character) + bias = adaptBias(delta, handled + 1, handled == basic) + delta = 0 + handled += 1 + } + } + delta += 1 + n += 1 + } + + return output + } + + /// Returns new string containing IDNA-encoded hostname + /// + /// - Returns: IDNA encoded hostname or nil if the string can't be encoded + public func encodeIDNA(_ input: Substring) -> String? { + let parts: [Substring] = input.split(separator: ".") + var output: String = "" + for part: Substring in parts { + if output.count > 0 { + output.append(".") + } + if part.rangeOfCharacter(from: CharacterSet.urlHostAllowed.inverted) != nil { + guard let encoded: String = part.lowercased().punycodeEncoded else { return nil } + output += ace + encoded + } else { + output += part + } + } + return output + } + + /// Returns new string containing hostname decoded from IDNA representation + /// + /// - Returns: Original hostname or nil if the string doesn't contain correct encoding + public func decodedIDNA(_ input: Substring) -> String? { + let parts: [Substring] = input.split(separator: ".") + var output: String = "" + for part: Substring in parts { + if output.count > 0 { + output.append(".") + } + if part.hasPrefix(ace) { + guard let decoded: String = part.dropFirst(ace.count).punycodeDecoded else { return nil } + output += decoded + } else { + output += part + } + } + return output + } +} + +private extension Substring { + func lastIndex(of element: Character) -> String.Index? { + var position: Index = endIndex + while position > startIndex { + position = self.index(before: position) + if self[position] == element { + return position + } + } + return nil + } +} + +private extension UnicodeScalar { + var isValid: Bool { + return value < 0xD880 || (value >= 0xE000 && value <= 0x1FFFFF) + } +} + +public extension Substring { + /// Returns new string in punycode encoding (RFC 3492) + /// + /// - Returns: Punycode encoded string or nil if the string can't be encoded + var punycodeEncoded: String? { + return Punycode().encodePunycode(self) + } + + /// Returns new string decoded from punycode representation (RFC 3492) + /// + /// - Returns: Original string or nil if the string doesn't contain correct encoding + var punycodeDecoded: String? { + return Punycode().decodePunycode(self) + } + + /// Returns new string containing IDNA-encoded hostname + /// + /// - Returns: IDNA encoded hostname or nil if the string can't be encoded + var idnaEncoded: String? { + return Punycode().encodeIDNA(self) + } + + /// Returns new string containing hostname decoded from IDNA representation + /// + /// - Returns: Original hostname or nil if the string doesn't contain correct encoding + var idnaDecoded: String? { + return Punycode().decodedIDNA(self) + } +} + +public extension String { + + /// Returns new string in punycode encoding (RFC 3492) + /// + /// - Returns: Punycode encoded string or nil if the string can't be encoded + var punycodeEncoded: String? { + return self[.. Bool { +public func isValidUrl(_ url: String, validSchemes: [String: Bool] = ["http": true, "https": true, "tonsite": true]) -> Bool { if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: escapedUrl), let scheme = url.scheme?.lowercased(), let requiresTopLevelDomain = validSchemes[scheme], let host = url.host, (!requiresTopLevelDomain || host.contains(".")) && url.user == nil { if requiresTopLevelDomain { let components = host.components(separatedBy: ".") @@ -39,8 +39,12 @@ public func isValidUrl(_ url: String, validSchemes: [String: Bool] = ["http": tr public func explicitUrl(_ url: String) -> String { var url = url - if !url.hasPrefix("http") && !url.hasPrefix("https") && url.range(of: "://") == nil { - url = "https://\(url)" + if !url.lowercased().hasPrefix("http:") && !url.lowercased().hasPrefix("https:") && !url.lowercased().hasPrefix("tonsite:") && url.range(of: "://") == nil { + if let parsedUrl = URL(string: "http://\(url)"), parsedUrl.host?.hasSuffix(".ton") == true { + url = "tonsite://\(url)" + } else { + url = "https://\(url)" + } } return url } diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index eff23dab0f7..bc5e329a88e 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -75,6 +75,7 @@ public enum ParsedInternalPeerUrlParameter { case story(Int32) case boost case text(String) + case profile } public enum ParsedInternalUrl { @@ -130,10 +131,8 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String) if !pathComponents.isEmpty && !pathComponents[0].isEmpty { let peerName: String = pathComponents[0] - if sharedContext.immediateExperimentalUISettings.browserExperiment { - if query.hasPrefix("ipfs/") { - return .externalUrl(url: "ipfs://" + String(query[query.index(query.startIndex, offsetBy: "ipfs/".count)...])) - } + if query.hasPrefix("tonsite/") { + return .externalUrl(url: "tonsite://" + String(query[query.index(query.startIndex, offsetBy: "tonsite/".count)...])) } if pathComponents[0].hasPrefix("+") || pathComponents[0].hasPrefix("%20") { @@ -293,7 +292,20 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String) } } return .startAttach(peerName, value, choose) - } else if queryItem.name == "story" { + } else if queryItem.name == "startapp" { + var compact = false + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "mode", value == "compact" { + compact = true + break + } + } + } + } + return .peer(.name(peerName), .appStart("", queryItem.value, compact)) + } else if queryItem.name == "story" { if let id = Int32(value) { return .peer(.name(peerName), .story(id)) } @@ -320,6 +332,21 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String) return .peer(.name(peerName), .groupBotStart("", botAdminRights)) } else if queryItem.name == "boost" { return .peer(.name(peerName), .boost) + } else if queryItem.name == "profile" { + return .peer(.name(peerName), .profile) + } else if queryItem.name == "startapp" { + var compact = false + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "mode", value == "compact" { + compact = true + break + } + } + } + } + return .peer(.name(peerName), .appStart("", nil, compact)) } } } @@ -690,6 +717,8 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) if let peer = peer { if let parameter = parameter { switch parameter { + case .profile: + return .single(.result(.peer(peer._asPeer(), .info(nil)))) case let .text(text): var textInputState: ChatTextInputState? if !text.isEmpty { @@ -717,18 +746,26 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } } case let .appStart(name, payload, compact): - return .single(.progress) |> then(context.engine.messages.getBotApp(botId: peer.id, shortName: name, cached: false) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { botApp -> Signal in - if let botApp { - return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: botApp, payload: payload, justInstalled: false, compact: compact))))) + if name.isEmpty { + if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) { + return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: nil, payload: payload, justInstalled: false, compact: compact))))) } else { return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)))) } - }) + } else { + return .single(.progress) |> then(context.engine.messages.getBotApp(botId: peer.id, shortName: name, cached: false) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { botApp -> Signal in + if let botApp { + return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: botApp, payload: payload, justInstalled: false, compact: compact))))) + } else { + return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)))) + } + }) + } case let .channelMessage(id, timecode): if case let .channel(channel) = peer, channel.flags.contains(.isForum) { let messageId = MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: id) @@ -924,7 +961,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .result(.replyThreadMessage(replyThreadMessage: result, messageId: messageId)) }) } else { - return .single(.result(.peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode), peekData: nil)))) + return .single(.result(.peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode, setupReply: false), peekData: nil)))) } } else { return .single(.result(.inaccessiblePeer)) @@ -1200,7 +1237,11 @@ public func resolveUrlImpl(context: AccountContext, peerId: PeerId?, url: String var url = url if !url.contains("://") && !url.hasPrefix("tel:") && !url.hasPrefix("mailto:") && !url.hasPrefix("calshow:") { if !(url.hasPrefix("http") || url.hasPrefix("https")) { - url = "http://\(url)" + if let mappedURL = URL(string: "https://\(url)"), let host = mappedURL.host, host.lowercased().hasSuffix(".ton") { + url = "tonsite://\(url)" + } else { + url = "http://\(url)" + } } } @@ -1292,3 +1333,18 @@ public func resolveInstantViewUrl(account: Account, url: String) -> Signal (domain: String, fullUrl: String) { + if let parsedUrl = URL(string: url) { + let host: String? + let scheme = parsedUrl.scheme ?? "https" + if #available(iOS 16.0, *) { + host = parsedUrl.host(percentEncoded: true)?.lowercased() + } else { + host = parsedUrl.host?.lowercased() + } + return (host ?? url, "\(scheme)://\(host ?? "")") + } else { + return (url, url) + } +} diff --git a/submodules/Utils/DeviceModel/BUILD b/submodules/Utils/DeviceModel/BUILD new file mode 100644 index 00000000000..16d3d3f3563 --- /dev/null +++ b/submodules/Utils/DeviceModel/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "DeviceModel", + module_name = "DeviceModel", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/LegacyComponents", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Utils/DeviceModel/Sources/DeviceModel.swift b/submodules/Utils/DeviceModel/Sources/DeviceModel.swift new file mode 100644 index 00000000000..ba30871c3b0 --- /dev/null +++ b/submodules/Utils/DeviceModel/Sources/DeviceModel.swift @@ -0,0 +1,368 @@ +import Foundation + +public enum DeviceModel: CaseIterable, Equatable { + public static var allCases: [DeviceModel] { + return [ + .iPodTouch1, + .iPodTouch2, + .iPodTouch3, + .iPodTouch4, + .iPodTouch5, + .iPodTouch6, + .iPodTouch7, + .iPhone, + .iPhone3G, + .iPhone3GS, + .iPhone4, + .iPhone4S, + .iPhone5, + .iPhone5C, + .iPhone5S, + .iPhone6, + .iPhone6Plus, + .iPhone6S, + .iPhone6SPlus, + .iPhoneSE, + .iPhone7, + .iPhone7Plus, + .iPhone8, + .iPhone8Plus, + .iPhoneX, + .iPhoneXS, + .iPhoneXR, + .iPhone11, + .iPhone11Pro, + .iPhone11ProMax, + .iPhone12, + .iPhone12Mini, + .iPhone12Pro, + .iPhone12ProMax, + .iPhone13, + .iPhone13Mini, + .iPhone13Pro, + .iPhone13ProMax, + .iPhone14, + .iPhone14Plus, + .iPhone14Pro, + .iPhone14ProMax, + .iPhone15, + .iPhone15Plus, + .iPhone15Pro, + .iPhone15ProMax + ] + } + + case iPodTouch1 + case iPodTouch2 + case iPodTouch3 + case iPodTouch4 + case iPodTouch5 + case iPodTouch6 + case iPodTouch7 + + case iPhone + case iPhone3G + case iPhone3GS + + case iPhone4 + case iPhone4S + + case iPhone5 + case iPhone5C + case iPhone5S + + case iPhone6 + case iPhone6Plus + case iPhone6S + case iPhone6SPlus + + case iPhoneSE + + case iPhone7 + case iPhone7Plus + case iPhone8 + case iPhone8Plus + + case iPhoneX + case iPhoneXS + case iPhoneXSMax + case iPhoneXR + + case iPhone11 + case iPhone11Pro + case iPhone11ProMax + + case iPhoneSE2ndGen + + case iPhone12 + case iPhone12Mini + case iPhone12Pro + case iPhone12ProMax + + case iPhone13 + case iPhone13Mini + case iPhone13Pro + case iPhone13ProMax + + case iPhoneSE3rdGen + + case iPhone14 + case iPhone14Plus + case iPhone14Pro + case iPhone14ProMax + + case iPhone15 + case iPhone15Plus + case iPhone15Pro + case iPhone15ProMax + + case unknown(String) + + public var modelId: [String] { + switch self { + case .iPodTouch1: + return ["iPod1,1"] + case .iPodTouch2: + return ["iPod2,1"] + case .iPodTouch3: + return ["iPod3,1"] + case .iPodTouch4: + return ["iPod4,1"] + case .iPodTouch5: + return ["iPod5,1"] + case .iPodTouch6: + return ["iPod7,1"] + case .iPodTouch7: + return ["iPod9,1"] + case .iPhone: + return ["iPhone1,1"] + case .iPhone3G: + return ["iPhone1,2"] + case .iPhone3GS: + return ["iPhone2,1"] + case .iPhone4: + return ["iPhone3,1", "iPhone3,2", "iPhone3,3"] + case .iPhone4S: + return ["iPhone4,1", "iPhone4,2", "iPhone4,3"] + case .iPhone5: + return ["iPhone5,1", "iPhone5,2"] + case .iPhone5C: + return ["iPhone5,3", "iPhone5,4"] + case .iPhone5S: + return ["iPhone6,1", "iPhone6,2"] + case .iPhone6: + return ["iPhone7,2"] + case .iPhone6Plus: + return ["iPhone7,1"] + case .iPhone6S: + return ["iPhone8,1"] + case .iPhone6SPlus: + return ["iPhone8,2"] + case .iPhoneSE: + return ["iPhone8,4"] + case .iPhone7: + return ["iPhone9,1", "iPhone9,3"] + case .iPhone7Plus: + return ["iPhone9,2", "iPhone9,4"] + case .iPhone8: + return ["iPhone10,1", "iPhone10,4"] + case .iPhone8Plus: + return ["iPhone10,2", "iPhone10,5"] + case .iPhoneX: + return ["iPhone10,3", "iPhone10,6"] + case .iPhoneXS: + return ["iPhone11,2"] + case .iPhoneXSMax: + return ["iPhone11,4", "iPhone11,6"] + case .iPhoneXR: + return ["iPhone11,8"] + case .iPhone11: + return ["iPhone12,1"] + case .iPhone11Pro: + return ["iPhone12,3"] + case .iPhone11ProMax: + return ["iPhone12,5"] + case .iPhoneSE2ndGen: + return ["iPhone12,8"] + case .iPhone12: + return ["iPhone13,2"] + case .iPhone12Mini: + return ["iPhone13,1"] + case .iPhone12Pro: + return ["iPhone13,3"] + case .iPhone12ProMax: + return ["iPhone13,4"] + case .iPhone13: + return ["iPhone14,5"] + case .iPhone13Mini: + return ["iPhone14,4"] + case .iPhone13Pro: + return ["iPhone14,2"] + case .iPhone13ProMax: + return ["iPhone14,3"] + case .iPhoneSE3rdGen: + return ["iPhone14,6"] + case .iPhone14: + return ["iPhone14,7"] + case .iPhone14Plus: + return ["iPhone14,8"] + case .iPhone14Pro: + return ["iPhone15,2"] + case .iPhone14ProMax: + return ["iPhone15,3"] + case .iPhone15: + return ["iPhone15,4"] + case .iPhone15Plus: + return ["iPhone15,5"] + case .iPhone15Pro: + return ["iPhone16,1"] + case .iPhone15ProMax: + return ["iPhone16,2"] + case let .unknown(modelId): + return [modelId] + } + } + + public var modelName: String { + switch self { + case .iPodTouch1: + return "iPod touch 1G" + case .iPodTouch2: + return "iPod touch 2G" + case .iPodTouch3: + return "iPod touch 3G" + case .iPodTouch4: + return "iPod touch 4G" + case .iPodTouch5: + return "iPod touch 5G" + case .iPodTouch6: + return "iPod touch 6G" + case .iPodTouch7: + return "iPod touch 7G" + case .iPhone: + return "iPhone" + case .iPhone3G: + return "iPhone 3G" + case .iPhone3GS: + return "iPhone 3GS" + case .iPhone4: + return "iPhone 4" + case .iPhone4S: + return "iPhone 4S" + case .iPhone5: + return "iPhone 5" + case .iPhone5C: + return "iPhone 5C" + case .iPhone5S: + return "iPhone 5S" + case .iPhone6: + return "iPhone 6" + case .iPhone6Plus: + return "iPhone 6 Plus" + case .iPhone6S: + return "iPhone 6S" + case .iPhone6SPlus: + return "iPhone 6S Plus" + case .iPhoneSE: + return "iPhone SE" + case .iPhone7: + return "iPhone 7" + case .iPhone7Plus: + return "iPhone 7 Plus" + case .iPhone8: + return "iPhone 8" + case .iPhone8Plus: + return "iPhone 8 Plus" + case .iPhoneX: + return "iPhone X" + case .iPhoneXS: + return "iPhone XS" + case .iPhoneXSMax: + return "iPhone XS Max" + case .iPhoneXR: + return "iPhone XR" + case .iPhone11: + return "iPhone 11" + case .iPhone11Pro: + return "iPhone 11 Pro" + case .iPhone11ProMax: + return "iPhone 11 Pro Max" + case .iPhoneSE2ndGen: + return "iPhone SE (2nd gen)" + case .iPhone12: + return "iPhone 12" + case .iPhone12Mini: + return "iPhone 12 mini" + case .iPhone12Pro: + return "iPhone 12 Pro" + case .iPhone12ProMax: + return "iPhone 12 Pro Max" + case .iPhone13: + return "iPhone 13" + case .iPhone13Mini: + return "iPhone 13 mini" + case .iPhone13Pro: + return "iPhone 13 Pro" + case .iPhone13ProMax: + return "iPhone 13 Pro Max" + case .iPhoneSE3rdGen: + return "iPhone SE (3rd gen)" + case .iPhone14: + return "iPhone 14" + case .iPhone14Plus: + return "iPhone 14 Plus" + case .iPhone14Pro: + return "iPhone 14 Pro" + case .iPhone14ProMax: + return "iPhone 14 Pro Max" + case .iPhone15: + return "iPhone 15" + case .iPhone15Plus: + return "iPhone 15 Plus" + case .iPhone15Pro: + return "iPhone 15 Pro" + case .iPhone15ProMax: + return "iPhone 15 Pro Max" + case let .unknown(modelId): + if modelId.hasPrefix("iPhone") { + return "Unknown iPhone" + } else if modelId.hasPrefix("iPod") { + return "Unknown iPod" + } else if modelId.hasPrefix("iPad") { + return "Unknown iPad" + } else { + return "Unknown Device" + } + } + } + + public var isIpad: Bool { + return self.modelId.first?.hasPrefix("iPad") ?? false + } + + public static let current = DeviceModel() + + private init() { + var systemInfo = utsname() + uname(&systemInfo) + let modelCode = withUnsafePointer(to: &systemInfo.machine) { + $0.withMemoryRebound(to: CChar.self, capacity: 1) { + ptr in String.init(validatingUTF8: ptr) + } + } + var result: DeviceModel? + if let modelCode { + for model in DeviceModel.allCases { + if model.modelId.contains(modelCode) { + result = model + break + } + } + } + if let result { + self = result + } else { + self = .unknown(modelCode ?? "") + } + } +} diff --git a/submodules/WatchBridge/Sources/WatchBridge.swift b/submodules/WatchBridge/Sources/WatchBridge.swift index 4cceb2c3483..c7c75e42e16 100644 --- a/submodules/WatchBridge/Sources/WatchBridge.swift +++ b/submodules/WatchBridge/Sources/WatchBridge.swift @@ -172,7 +172,7 @@ func makeBridgeMedia(message: Message, strings: PresentationStrings, chatPeer: P for attribute in file.attributes { switch attribute { - case let .Video(duration, size, flags, _): + case let .Video(duration, size, flags, _, _): bridgeVideo.duration = Int32(duration) bridgeVideo.dimensions = size.cgSize bridgeVideo.round = flags.contains(.instantRoundVideo) diff --git a/submodules/WebSearchUI/Sources/WebSearchController.swift b/submodules/WebSearchUI/Sources/WebSearchController.swift index 079887e0f86..803e75a9299 100644 --- a/submodules/WebSearchUI/Sources/WebSearchController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchController.swift @@ -590,25 +590,6 @@ public class WebSearchPickerContext: AttachmentMediaPickerContext { } } - public var hasCaption: Bool { - return false - } - - public var captionIsAboveMedia: Signal { - return .single(false) - } - - public func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { - } - - public var loadingProgress: Signal { - return .single(nil) - } - - public var mainButtonState: Signal { - return .single(nil) - } - init(interaction: WebSearchControllerInteraction) { self.interaction = interaction } @@ -624,8 +605,4 @@ public class WebSearchPickerContext: AttachmentMediaPickerContext { public func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { self.interaction?.schedule(parameters) } - - public func mainButtonAction() { - - } } diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift index 2dd6561a203..f6b47151423 100644 --- a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift @@ -37,7 +37,7 @@ struct WebSearchGalleryEntry: Equatable { switch self.result { case let .externalReference(externalReference): if let content = externalReference.content, externalReference.type == "gif", let thumbnailResource = externalReference.thumbnail?.resource, let dimensions = content.dimensions { - let fileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil)])) + let fileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil)])) return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), userLocation: .other, fileReference: fileReference, loopVideo: true, enableSound: false, fetchAutomatically: true, storeAfterDownload: nil), controllerInteraction: controllerInteraction) } case let .internalReference(internalReference): diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index f24a3459d0c..16405520eba 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -18,7 +18,6 @@ swift_library( "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/AccountContext:AccountContext", "//submodules/AttachmentUI:AttachmentUI", - "//submodules/CounterControllerTitleView:CounterControllerTitleView", "//submodules/HexColor:HexColor", "//submodules/PhotoResources:PhotoResources", "//submodules/ShimmerEffect:ShimmerEffect", @@ -34,7 +33,14 @@ swift_library( "//submodules/Markdown:Markdown", "//submodules/TextFormat:TextFormat", "//submodules/LocalAuth", - "//submodules/InstantPageCache" + "//submodules/InstantPageCache", + "//submodules/OpenInExternalAppUI", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/ShareController", + "//submodules/UndoUI", + "//submodules/OverlayStatusController", + "//submodules/TelegramUIPreferences", ], visibility = [ "//visibility:public", diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 574919ba3f9..954c427f964 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -9,7 +9,6 @@ import SwiftSignalKit import TelegramPresentationData import AccountContext import AttachmentUI -import CounterControllerTitleView import ContextUI import PresentationDataUtils import HexColor @@ -25,164 +24,19 @@ import QrCodeUI import InstantPageUI import InstantPageCache import LocalAuth +import OpenInExternalAppUI +import ShareController +import UndoUI +import AvatarNode +import OverlayStatusController +import TelegramUIPreferences private let durgerKingBotIds: [Int64] = [5104055776, 2200339955] -public class WebAppCancelButtonNode: ASDisplayNode { - public enum State { - case cancel - case back - } - - public let buttonNode: HighlightTrackingButtonNode - private let arrowNode: ASImageNode - private let labelNode: ImmediateTextNode - - public var state: State = .cancel - - private var color: UIColor? - - private var _theme: PresentationTheme - public var theme: PresentationTheme { - get { - return self._theme - } - set { - self._theme = newValue - self.setState(self.state, animated: false, animateScale: false, force: true) - } - } - private let strings: PresentationStrings - - private weak var colorSnapshotView: UIView? - - public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) { - let previousColor = self.color - self.color = color - - if case let .animated(duration, curve) = transition, previousColor != color, !self.animatingStateChange { - if let snapshotView = self.view.snapshotContentTree() { - snapshotView.frame = self.bounds - self.view.addSubview(snapshotView) - self.colorSnapshotView = snapshotView - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in - snapshotView.removeFromSuperview() - }) - self.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) - self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) - } - } - self.setState(self.state, animated: false, animateScale: false, force: true) - } - - public init(theme: PresentationTheme, strings: PresentationStrings) { - self._theme = theme - self.strings = strings - - self.buttonNode = HighlightTrackingButtonNode() - - self.arrowNode = ASImageNode() - self.arrowNode.displaysAsynchronously = false - - self.labelNode = ImmediateTextNode() - self.labelNode.displaysAsynchronously = false - - super.init() - - self.addSubnode(self.buttonNode) - self.buttonNode.addSubnode(self.arrowNode) - self.buttonNode.addSubnode(self.labelNode) - - self.buttonNode.highligthedChanged = { [weak self] highlighted in - guard let strongSelf = self else { - return - } - if highlighted { - strongSelf.arrowNode.layer.removeAnimation(forKey: "opacity") - strongSelf.arrowNode.alpha = 0.4 - strongSelf.labelNode.layer.removeAnimation(forKey: "opacity") - strongSelf.labelNode.alpha = 0.4 - } else { - strongSelf.arrowNode.alpha = 1.0 - strongSelf.arrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - strongSelf.labelNode.alpha = 1.0 - strongSelf.labelNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - - self.setState(.cancel, animated: false, force: true) - } - - public func setTheme(_ theme: PresentationTheme, animated: Bool) { - self._theme = theme - var animated = animated - if self.animatingStateChange { - animated = false - } - self.setState(self.state, animated: animated, animateScale: false, force: true) - } - - private var animatingStateChange = false - public func setState(_ state: State, animated: Bool, animateScale: Bool = true, force: Bool = false) { - guard self.state != state || force else { - return - } - self.state = state - - if let colorSnapshotView = self.colorSnapshotView { - self.colorSnapshotView = nil - colorSnapshotView.removeFromSuperview() - } - - if animated, let snapshotView = self.buttonNode.view.snapshotContentTree() { - self.animatingStateChange = true - snapshotView.layer.sublayerTransform = self.buttonNode.subnodeTransform - self.view.addSubview(snapshotView) - - let duration: Double = animateScale ? 0.25 : 0.3 - if animateScale { - snapshotView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.25, removeOnCompletion: false) - } - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - self.animatingStateChange = false - }) - - if animateScale { - self.buttonNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.25) - } - self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) - } - - let color = self.color ?? self.theme.rootController.navigationBar.accentTextColor - - self.arrowNode.isHidden = state == .cancel - self.labelNode.attributedText = NSAttributedString(string: state == .cancel ? self.strings.Common_Cancel : self.strings.Common_Back, font: Font.regular(17.0), textColor: color) - - let labelSize = self.labelNode.updateLayout(CGSize(width: 120.0, height: 56.0)) - - self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: labelSize.width, height: self.buttonNode.frame.height)) - self.arrowNode.image = NavigationBarTheme.generateBackArrowImage(color: color) - if let image = self.arrowNode.image { - self.arrowNode.frame = CGRect(origin: self.arrowNode.frame.origin, size: image.size) - } - self.labelNode.frame = CGRect(origin: self.labelNode.frame.origin, size: labelSize) - self.buttonNode.subnodeTransform = CATransform3DMakeTranslation(state == .back ? 11.0 : 0.0, 0.0, 0.0) - } - - override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: self.buttonNode.frame.width, height: constrainedSize.height)) - self.arrowNode.frame = CGRect(origin: CGPoint(x: -19.0, y: floorToScreenPixels((constrainedSize.height - self.arrowNode.frame.size.height) / 2.0)), size: self.arrowNode.frame.size) - self.labelNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((constrainedSize.height - self.labelNode.frame.size.height) / 2.0)), size: self.labelNode.frame.size) - - return CGSize(width: 70.0, height: 56.0) - } -} - public struct WebAppParameters { public enum Source { case generic + case button case menu case attachMenu case inline @@ -202,6 +56,7 @@ public struct WebAppParameters { let peerId: PeerId let botId: PeerId let botName: String + let botVerified: Bool let url: String? let queryId: Int64? let payload: String? @@ -215,6 +70,7 @@ public struct WebAppParameters { peerId: PeerId, botId: PeerId, botName: String, + botVerified: Bool, url: String?, queryId: Int64?, payload: String?, @@ -227,6 +83,7 @@ public struct WebAppParameters { self.peerId = peerId self.botId = botId self.botName = botName + self.botVerified = botVerified self.url = url self.queryId = queryId self.payload = payload @@ -267,7 +124,7 @@ public final class WebAppController: ViewController, AttachmentContainable { public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } - + fileprivate class Node: ViewControllerTracingNode, WKNavigationDelegate, WKUIDelegate, ASScrollViewDelegate { private weak var controller: WebAppController? @@ -293,12 +150,13 @@ public final class WebAppController: ViewController, AttachmentContainable { private var queryId: Int64? fileprivate let canMinimize = true - private var placeholderDisposable: Disposable? - private var iconDisposable: Disposable? + private var placeholderDisposable = MetaDisposable() private var keepAliveDisposable: Disposable? - private var paymentDisposable: Disposable? + private var iconDisposable: Disposable? + fileprivate var icon: UIImage? + private var lastExpansionTimestamp: Double? private var didTransitionIn = false @@ -389,7 +247,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } } - self.placeholderDisposable = (placeholder + self.placeholderDisposable.set((placeholder |> deliverOnMainQueue).start(next: { [weak self] fileReferenceAndIsPlaceholder in guard let strongSelf = self else { return @@ -407,7 +265,7 @@ public final class WebAppController: ViewController, AttachmentContainable { if let fileReference = fileReference { let _ = freeMediaFileInteractiveFetched(account: strongSelf.context.account, userLocation: .other, fileReference: fileReference).start() } - strongSelf.iconDisposable = (svgIconImageFile(account: strongSelf.context.account, fileReference: fileReference, stickToTop: isPlaceholder) + strongSelf.placeholderDisposable.set((svgIconImageFile(account: strongSelf.context.account, fileReference: fileReference, stickToTop: isPlaceholder) |> deliverOnMainQueue).start(next: { [weak self] transform in if let strongSelf = self { let imageSize: CGSize @@ -427,13 +285,27 @@ public final class WebAppController: ViewController, AttachmentContainable { } strongSelf.placeholderNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - }) + })) + })) + + self.iconDisposable = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId)) + |> mapToSignal { peer -> Signal in + guard let peer else { + return .complete() + } + return peerAvatarCompleteImage(account: context.account, peer: peer, size: CGSize(width: 32.0, height: 32.0), round: false) + } + |> deliverOnMainQueue).start(next: { [weak self] icon in + guard let self else { + return + } + self.icon = icon }) } deinit { - self.placeholderDisposable?.dispose() self.iconDisposable?.dispose() + self.placeholderDisposable.dispose() self.keepAliveDisposable?.dispose() self.paymentDisposable?.dispose() @@ -456,12 +328,12 @@ public final class WebAppController: ViewController, AttachmentContainable { guard let controller = self.controller else { return } + if let url = controller.url, controller.source != .menu { self.queryId = controller.queryId if let parsedUrl = URL(string: url) { self.webView?.load(URLRequest(url: parsedUrl)) } - if let keepAliveSignal = controller.keepAliveSignal { self.keepAliveDisposable = (keepAliveSignal |> deliverOnMainQueue).start(error: { [weak self] _ in @@ -493,16 +365,16 @@ public final class WebAppController: ViewController, AttachmentContainable { guard let self, let controller = self.controller else { return } - guard case let .peer(peer, params) = result, let peer, case let .withBotApp(appStart) = params else { + guard case let .peer(peer, params) = result, let peer, case let .withBotApp(appStart) = params, let botApp = appStart.botApp else { controller.dismiss() return } - let _ = (self.context.engine.messages.requestAppWebView(peerId: peer.id, appReference: .id(id: appStart.botApp.id, accessHash: appStart.botApp.accessHash), payload: appStart.payload, themeParams: generateWebAppThemeParams(self.presentationData.theme), compact: appStart.compact, allowWrite: true) + let _ = (self.context.engine.messages.requestAppWebView(peerId: peer.id, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: appStart.payload, themeParams: generateWebAppThemeParams(self.presentationData.theme), compact: appStart.compact, allowWrite: true) |> deliverOnMainQueue).startStandalone(next: { [weak self] result in guard let self, let parsedUrl = URL(string: result.url) else { return } - self.controller?.titleView?.title = CounterControllerTitle(title: appStart.botApp.title, counter: self.presentationData.strings.Bot_GenericBotStatus) + self.controller?.titleView?.title = WebAppTitle(title: botApp.title, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: controller.botVerified) self.webView?.load(URLRequest(url: parsedUrl)) }) }) @@ -514,7 +386,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } strongSelf.queryId = result.queryId strongSelf.webView?.load(URLRequest(url: parsedUrl)) - + if let keepAliveSignal = result.keepAliveSignal { strongSelf.keepAliveDisposable = (keepAliveSignal |> deliverOnMainQueue).start(error: { [weak self] _ in @@ -941,6 +813,8 @@ public final class WebAppController: ViewController, AttachmentContainable { } let tryInstantView = json["try_instant_view"] as? Bool ?? false + let tryBrowser = json["try_browser"] as? String + if let lastTouchTimestamp = self.webView?.lastTouchTimestamp, currentTimestamp < lastTouchTimestamp + 10.0 { self.webView?.lastTouchTimestamp = nil if tryInstantView { @@ -957,13 +831,47 @@ public final class WebAppController: ViewController, AttachmentContainable { } switch result { case let .instantView(webPage, anchor): - let controller = InstantPageController(context: strongSelf.context, webPage: webPage, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .otherPrivate), anchor: anchor) + let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, webPage: webPage, anchor: anchor, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .otherPrivate)) strongSelf.controller?.getNavigationController()?.pushViewController(controller) default: strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) } }) } else { + var url = url + if let tryBrowser { + let openInOptions = availableOpenInOptions(context: self.context, item: .url(url: url)) + var matchingOption: OpenInOption? + for option in openInOptions { + if case .other = option.application { + switch tryBrowser { + case "safari": + break + case "chrome": + if option.identifier == "chrome" { + matchingOption = option + break + } + case "firefox": + if ["firefox", "firefoxFocus"].contains(option.identifier) { + matchingOption = option + break + } + case "opera": + if ["operaMini", "operaTouch"].contains(option.identifier) { + matchingOption = option + break + } + default: + break + } + } + } + if let matchingOption, case let .openUrl(newUrl) = matchingOption.action() { + url = newUrl + } + } + self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) } } @@ -1174,6 +1082,90 @@ public final class WebAppController: ViewController, AttachmentContainable { self.openBotSettings() } + case "web_app_setup_swipe_behavior": + if let json = json, let isPanGestureEnabled = json["allow_vertical_swipe"] as? Bool { + self.controller?._isPanGestureEnabled = isPanGestureEnabled + } + case "web_app_share_to_story": + if let json = json, let mediaUrl = json["media_url"] as? String { + let text = json["text"] as? String + let link = json["widget_link"] as? [String: Any] + + var linkUrl: String? + var linkName: String? + if let link { + if let url = link["url"] as? String { + linkUrl = url + if let name = link["name"] as? String { + linkName = name + } + } + } + + enum FetchResult { + case result(Data) + case progress(Float) + } + + let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: { + })) + self.controller?.present(controller, in: .window(.root)) + + let _ = (fetchHttpResource(url: mediaUrl) + |> map(Optional.init) + |> `catch` { error in + return .single(nil) + } + |> mapToSignal { value -> Signal in + if case let .dataPart(_, data, _, complete) = value, complete { + return .single(.result(data)) + } else if case let .progressUpdated(progress) = value { + return .single(.progress(progress)) + } else { + return .complete() + } + } + |> deliverOnMainQueue).start(next: { [weak self, weak controller] next in + guard let self else { + return + } + controller?.dismiss() + + switch next { + case let .result(data): + var source: Any? + if let image = UIImage(data: data) { + source = image + } else { + let tempFile = TempBox.shared.tempFile(fileName: "image.mp4") + if let _ = try? data.write(to: URL(fileURLWithPath: tempFile.path), options: .atomic) { + source = tempFile.path + } + } + if let source { + let externalState = MediaEditorTransitionOutExternalState( + storyTarget: nil, + isForcedTarget: false, + isPeerArchived: false, + transitionOut: nil + ) + let controller = self.context.sharedContext.makeStoryMediaEditorScreen(context: self.context, source: source, text: text, link: linkUrl.flatMap { ($0, linkName) }, completion: { result, commit in + let target: Stories.PendingTarget = result.target + externalState.storyTarget = target + + if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + } + }) + if let navigationController = self.controller?.getNavigationController() { + navigationController.pushViewController(controller) + } + } + default: + break + } + }) + } default: break } @@ -1797,7 +1789,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return self.displayNode as! Node } - private var titleView: CounterControllerTitleView? + private var titleView: WebAppTitleView? fileprivate let cancelButtonNode: WebAppCancelButtonNode fileprivate let moreButtonNode: MoreButtonNode @@ -1806,6 +1798,7 @@ public final class WebAppController: ViewController, AttachmentContainable { private let peerId: PeerId public let botId: PeerId private let botName: String + private let botVerified: Bool private let url: String? private let queryId: Int64? private let payload: String? @@ -1832,6 +1825,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.peerId = params.peerId self.botId = params.botId self.botName = params.botName + self.botVerified = params.botVerified self.url = params.url self.queryId = params.queryId self.payload = params.payload @@ -1868,8 +1862,10 @@ public final class WebAppController: ViewController, AttachmentContainable { self.navigationItem.rightBarButtonItem?.action = #selector(self.moreButtonPressed) self.navigationItem.rightBarButtonItem?.target = self - let titleView = CounterControllerTitleView(theme: self.presentationData.theme) - titleView.title = CounterControllerTitle(title: params.botName, counter: self.presentationData.strings.Bot_GenericBotStatus) + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + let titleView = WebAppTitleView(context: self.context, theme: self.presentationData.theme) + titleView.title = WebAppTitle(title: params.botName, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: params.botVerified) self.navigationItem.titleView = titleView self.titleView = titleView @@ -1912,7 +1908,7 @@ public final class WebAppController: ViewController, AttachmentContainable { switch self.source { case .generic, .settings: completion() - case .inline, .attachMenu, .menu, .simple: + case .button, .inline, .attachMenu, .menu, .simple: let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId) ) @@ -1920,9 +1916,9 @@ public final class WebAppController: ViewController, AttachmentContainable { guard let self, let chatPeer else { return } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(chatPeer), completion: { _ in - completion() + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(chatPeer), keepStack: .always, completion: { _ in })) + completion() }) } } @@ -1969,7 +1965,12 @@ public final class WebAppController: ViewController, AttachmentContainable { @objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { let context = self.context - let presentationData = self.presentationData + var presentationData = self.presentationData + if !presentationData.theme.overallDarkAppearance, let headerColor = self.controllerNode.headerColor { + if headerColor.lightness < 0.5 { + presentationData = presentationData.withUpdated(theme: defaultDarkPresentationTheme) + } + } let peerId = self.peerId let botId = self.botId @@ -1978,9 +1979,13 @@ public final class WebAppController: ViewController, AttachmentContainable { let hasSettings = self.hasSettings - let items = context.engine.messages.attachMenuBots() + 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)) + ) |> take(1) - |> map { [weak self] attachMenuBots -> ContextController.Items in + |> map { [weak self] attachMenuBots, botPeer, botCommands -> ContextController.Items in var items: [ContextMenuItem] = [] let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == botId && !$0.flags.contains(.notActivated) }) @@ -2015,13 +2020,31 @@ public final class WebAppController: ViewController, AttachmentContainable { return } if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { - strongSelf.dismiss() + (strongSelf.parentController() as? AttachmentController)?.minimizeIfNeeded() strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(botPeer))) } }) }))) } + if let addressName = botPeer?.addressName { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_Share, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c?.dismiss(completion: nil) + + guard let self else { + return + } + let shareController = ShareController(context: context, subject: .url("https://t.me/\(addressName)?profile")) + shareController.actionCompleted = { [weak self] in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + self.present(shareController, in: .window(.root)) + }))) + } + items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_ReloadPage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reload"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in @@ -2029,7 +2052,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self?.controllerNode.webView?.reload() }))) - + items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_TermsOfUse, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in @@ -2049,6 +2072,28 @@ 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))) + } + }))) + } + } + } + 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) @@ -2070,7 +2115,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return ContextController.Items(content: .list(items)) } - let contextController = ContextController(presentationData: self.presentationData, source: .reference(WebAppContextReferenceContentSource(controller: self, sourceNode: node)), items: items, gesture: gesture) + let contextController = ContextController(presentationData: presentationData, source: .reference(WebAppContextReferenceContentSource(controller: self, sourceNode: node)), items: items, gesture: gesture) self.presentInGlobalOverlay(contextController) } @@ -2084,8 +2129,10 @@ public final class WebAppController: ViewController, AttachmentContainable { public func isContainerPanningUpdated(_ isPanning: Bool) { self.controllerNode.isContainerPanningUpdated(isPanning) } - + + private var validLayout: ContainerViewLayout? override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) @@ -2134,44 +2181,68 @@ public final class WebAppController: ViewController, AttachmentContainable { } } - public override var isMinimized: Bool { + public var isMinimized: Bool = false { didSet { - if self.isMinimized != oldValue && self.isMinimized { - self.controllerNode.webView?.hideScrollIndicators() + if self.isMinimized != oldValue { + if self.isMinimized { + self.controllerNode.webView?.hideScrollIndicators() + } else { + self.requestLayout(transition: .immediate) + self.controllerNode.webView?.setNeedsLayout() + } } } } - public func shouldDismissImmediately() -> Bool { + public var isMinimizable: Bool { return true } - fileprivate var canMinimize: Bool { - return self.controllerNode.canMinimize - } -} - -final class WebAppPickerContext: AttachmentMediaPickerContext { - private weak var controller: WebAppController? - - var selectionCount: Signal { - return .single(0) + public func shouldDismissImmediately() -> Bool { + if self.controllerNode.needDismissConfirmation { + return false + } else { + return true + } } - var caption: Signal { - return .single(nil) + fileprivate var _isPanGestureEnabled = true + public var isInnerPanGestureEnabled: (() -> Bool)? { + return { [weak self] in + guard let self else { + return true + } + return self._isPanGestureEnabled + } } - var hasCaption: Bool { - return false + fileprivate var canMinimize: Bool { + return self.controllerNode.canMinimize } - var captionIsAboveMedia: Signal { - return .single(false) + public var minimizedIcon: UIImage? { + return self.controllerNode.icon } - func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { + public func makeContentSnapshotView() -> UIView? { + guard let webView = self.controllerNode.webView, let _ = self.validLayout else { + return nil + } + + let configuration = WKSnapshotConfiguration() + configuration.rect = CGRect(origin: .zero, size: webView.frame.size) + + let imageView = UIImageView() + imageView.frame = CGRect(origin: .zero, size: webView.frame.size) + webView.takeSnapshot(with: configuration, completionHandler: { image, _ in + imageView.image = image + }) + return imageView } +} + +final class WebAppPickerContext: AttachmentMediaPickerContext { + private weak var controller: WebAppController? public var loadingProgress: Signal { return self.controller?.controllerNode.loadingProgressPromise.get() ?? .single(nil) @@ -2185,15 +2256,6 @@ final class WebAppPickerContext: AttachmentMediaPickerContext { self.controller = controller } - func setCaption(_ caption: NSAttributedString) { - } - - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { - } - - func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { - } - func mainButtonAction() { self.controller?.controllerNode.mainButtonPressed() } diff --git a/submodules/WebUI/Sources/WebAppTitleView.swift b/submodules/WebUI/Sources/WebAppTitleView.swift new file mode 100644 index 00000000000..627c1ba82f9 --- /dev/null +++ b/submodules/WebUI/Sources/WebAppTitleView.swift @@ -0,0 +1,159 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import ComponentFlow +import EmojiStatusComponent +import AccountContext + +public struct WebAppTitle: Equatable { + public var title: String + public var counter: String + public var isVerified: Bool + + public init(title: String, counter: String, isVerified: Bool) { + self.title = title + self.counter = counter + self.isVerified = isVerified + } +} + +public final class WebAppTitleView: UIView { + private let context: AccountContext + + private let titleNode: ImmediateTextNode + private let subtitleNode: ImmediateTextNode + private var titleCredibilityIconView: ComponentHostView? + + public var title: WebAppTitle = WebAppTitle(title: "", counter: "", isVerified: false) { + didSet { + if self.title != oldValue { + self.update() + } + } + } + + public var theme: PresentationTheme { + didSet { + self.update() + } + } + + private var primaryTextColor: UIColor? + private var secondaryTextColor: UIColor? + + public func updateTextColors(primary: UIColor?, secondary: UIColor?, transition: ContainedViewLayoutTransition) { + self.primaryTextColor = primary + self.secondaryTextColor = secondary + + if case let .animated(duration, curve) = transition { + if let snapshotView = self.snapshotContentTree() { + snapshotView.frame = self.bounds + self.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + } + } + + self.update() + } + + private func update() { + let primaryTextColor = self.primaryTextColor ?? self.theme.rootController.navigationBar.primaryTextColor + let secondaryTextColor = self.secondaryTextColor ?? self.theme.rootController.navigationBar.secondaryTextColor + self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: primaryTextColor) + self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.regular(13.0), textColor: secondaryTextColor) + + self.accessibilityLabel = self.title.title + self.accessibilityValue = self.title.counter + + self.setNeedsLayout() + } + + public init(context: AccountContext, theme: PresentationTheme) { + self.context = context + self.theme = theme + + self.titleNode = ImmediateTextNode() + self.titleNode.displaysAsynchronously = false + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.truncationType = .end + self.titleNode.isOpaque = false + + self.subtitleNode = ImmediateTextNode() + self.subtitleNode.displaysAsynchronously = false + self.subtitleNode.maximumNumberOfLines = 1 + self.subtitleNode.truncationType = .end + self.subtitleNode.isOpaque = false + + super.init(frame: CGRect()) + + self.isAccessibilityElement = true + self.accessibilityTraits = .header + + self.addSubnode(self.titleNode) + self.addSubnode(self.subtitleNode) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func layoutSubviews() { + super.layoutSubviews() + + let size = self.bounds.size + let spacing: CGFloat = 0.0 + + let titleSize = self.titleNode.updateLayout(CGSize(width: max(1.0, size.width), height: size.height)) + let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: max(1.0, size.width), height: size.height)) + let combinedHeight = titleSize.height + subtitleSize.height + spacing + + + var totalWidth = titleSize.width + + if self.title.isVerified { + let statusContent: EmojiStatusComponent.Content = .verified(fillColor: self.theme.list.itemCheckColors.fillColor, foregroundColor: self.theme.list.itemCheckColors.foregroundColor, sizeType: .large) + let titleCredibilityIconTransition: ComponentTransition = .immediate + + let titleCredibilityIconView: ComponentHostView + if let current = self.titleCredibilityIconView { + titleCredibilityIconView = current + } else { + titleCredibilityIconView = ComponentHostView() + self.titleCredibilityIconView = titleCredibilityIconView + self.addSubview(titleCredibilityIconView) + } + + let titleIconSize = titleCredibilityIconView.update( + transition: titleCredibilityIconTransition, + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.context.animationCache, + animationRenderer: self.context.animationRenderer, + content: statusContent, + isVisibleForAnimations: true, + action: { + } + )), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + + totalWidth += titleIconSize.width + 2.0 + + titleCredibilityIconTransition.setFrame(view: titleCredibilityIconView, frame: CGRect(origin: CGPoint(x:floorToScreenPixels((size.width - totalWidth) / 2.0) + titleSize.width + 2.0, y: floorToScreenPixels(floorToScreenPixels((size.height - combinedHeight) / 2.0 + titleSize.height / 2.0) - titleIconSize.height / 2.0)), size: titleIconSize)) + } + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0), y: floorToScreenPixels((size.height - combinedHeight) / 2.0)), size: titleSize) + self.titleNode.frame = titleFrame + + let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - subtitleSize.width) / 2.0), y: floorToScreenPixels((size.height - combinedHeight) / 2.0) + titleSize.height + spacing), size: subtitleSize) + self.subtitleNode.frame = subtitleFrame + } +} diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index 48c4d5dc8f2..27e3224a55f 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -175,6 +175,10 @@ final class WebAppWebView: WKWebView { fatalError("init(coder:) has not been implemented") } + deinit { + print() + } + override func didMoveToSuperview() { super.didMoveToSuperview() diff --git a/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift b/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift index a17977a4d44..e350f2ec7cc 100644 --- a/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift +++ b/submodules/WidgetItemsUtils/Sources/WidgetItemsUtils.swift @@ -22,7 +22,7 @@ public extension WidgetDataPeer.Message { switch attribute { case let .Sticker(altText, _, _): content = .sticker(WidgetDataPeer.Message.Content.Sticker(altText: altText)) - case let .Video(duration, _, flags, _): + case let .Video(duration, _, flags, _, _): if flags.contains(.instantRoundVideo) { content = .videoMessage(WidgetDataPeer.Message.Content.VideoMessage(duration: Int32(duration))) } else { diff --git a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh index 32239864f5e..be58ac5e4f9 100755 --- a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh +++ b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh @@ -47,13 +47,13 @@ CONFIGURE_FLAGS="--enable-cross-compile --disable-programs \ --enable-libopus \ --enable-libvpx \ --enable-audiotoolbox \ - --enable-bsf=aac_adtstoasc,vp9_superframe \ + --enable-bsf=aac_adtstoasc,vp9_superframe,h264_mp4toannexb \ --enable-decoder=h264,libvpx_vp9,hevc,libopus,mp3,aac,flac,alac_at,pcm_s16le,pcm_s24le,gsm_ms_at \ - --enable-encoder=libvpx_vp9 \ - --enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska \ + --enable-encoder=libvpx_vp9,aac_at \ + --enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska,mpegts \ --enable-parser=aac,h264,mp3,libopus \ --enable-protocol=file \ - --enable-muxer=mp4,matroska \ + --enable-muxer=mp4,matroska,mpegts \ " diff --git a/swift_deps.bzl b/swift_deps.bzl deleted file mode 100644 index 5c72264d3aa..00000000000 --- a/swift_deps.bzl +++ /dev/null @@ -1,322 +0,0 @@ -load("@rules_swift_package_manager//swiftpkg:defs.bzl", "swift_package") - -def swift_dependencies(): - # version: 0.6.7 - swift_package( - name = "swiftpkg_anycodable", - commit = "862808b2070cd908cb04f9aafe7de83d35f81b05", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/Flight-School/AnyCodable", - ) - - # version: 1.0.2 - swift_package( - name = "swiftpkg_bigdecimal", - commit = "04d17040e4615fbfda3a882b9881f6841f4bf557", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/Zollerboy1/BigDecimal.git", - ) - - # version: 5.3.0 - swift_package( - name = "swiftpkg_bigint", - commit = "0ed110f7555c34ff468e72e1686e59721f2b0da6", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/attaswift/BigInt", - ) - - # branch: release/1.0.0 - swift_package( - name = "swiftpkg_core_swift", - commit = "f03e7c89c56aaafdb3b22a8ab5ebfefb3fb018a6", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/denis15yo/core-swift.git", - ) - - # version: 1.8.2 - swift_package( - name = "swiftpkg_cryptoswift", - commit = "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/krzyzanowskim/CryptoSwift.git", - ) - - # version: 0.1.2 - swift_package( - name = "swiftpkg_curvelib.swift", - commit = "9f88bd5e56d1df443a908f7a7e81ae4f4d9170ea", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/tkey/curvelib.swift", - ) - - # version: 2.1.3 - swift_package( - name = "swiftpkg_factory", - commit = "587995f7d5cc667951d635fbf6b4252324ba0439", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/hmlongco/Factory.git", - ) - - # version: 5.2.0 - swift_package( - name = "swiftpkg_fetch_node_details_swift", - commit = "4bd96c33ba8d02d9e27190c5c7cedf09cfdfd656", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/torusresearch/fetch-node-details-swift.git", - ) - - # version: 2.6.1 - swift_package( - name = "swiftpkg_floatingpanel", - commit = "29185a47bd9f062c060e097641b863ef07f60ba7", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/scenee/FloatingPanel", - ) - - # branch: master - swift_package( - name = "swiftpkg_grdb.swift", - commit = "afc958017ee4feefd3c61c8e2cddf81d079d2e39", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/denis15yo/GRDB.swift.git", - ) - - # version: 20.0.0 - swift_package( - name = "swiftpkg_keychain_swift", - commit = "d108a1fa6189e661f91560548ef48651ed8d93b9", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/evgenyneu/keychain-swift.git", - ) - - # version: 1.2.0 - swift_package( - name = "swiftpkg_lnextensionexecutor", - commit = "c0226dcd7d653d4c22dd16ccd72619c86b610c2d", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/LeoNatan/LNExtensionExecutor", - ) - - # branch: main - swift_package( - name = "swiftpkg_navigation_stack_backport", - commit = "66716ce9c31198931c2275a0b69de2fdaa687e74", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/denis15yo/navigation-stack-backport.git", - ) - - # branch: develop - swift_package( - name = "swiftpkg_nicegram_assistant_ios", - commit = "033d4882165ee343a5db09c93cbca462f27f5732", - dependencies_index = "@//:swift_deps_index.json", - remote = "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", - ) - - # branch: develop - swift_package( - name = "swiftpkg_nicegram_wallet_ios", - commit = "2d647862a3111bfc0f6c0f9e966fdc8ff1a21d34", - dependencies_index = "@//:swift_deps_index.json", - remote = "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", - ) - - # version: 14.3.1 - swift_package( - name = "swiftpkg_qrcode", - commit = "263f280d2c8144adfb0b6676109846cfc8dd552b", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/WalletConnect/QRCode", - ) - - # version: 7.3.2 - swift_package( - name = "swiftpkg_r.swift", - commit = "4a0f8c97f1baa27d165dc801982c55bbf51126e5", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/denis15yo/R.swift.git", - ) - - # version: 5.15.5 - swift_package( - name = "swiftpkg_sdwebimage", - commit = "86e9185ef41c4238a93ad8efe61ddeb701e80bbf", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/SDWebImage/SDWebImage.git", - ) - - # version: 3.1.1 - swift_package( - name = "swiftpkg_session_manager_swift", - commit = "20cc7bff065d7fe53164d17e7714a3f17d4cea2a", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/Web3Auth/session-manager-swift.git", - ) - - # version: 4.0.0 - swift_package( - name = "swiftpkg_single_factor_auth_swift", - commit = "4caaaa858950b25ea420dbba79de6b4c58801db4", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/Web3Auth/single-factor-auth-swift.git", - ) - - # version: 5.6.0 - swift_package( - name = "swiftpkg_snapkit", - commit = "2842e6e84e82eb9a8dac0100ca90d9444b0307f4", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/SnapKit/SnapKit.git", - ) - - # version: 4.0.8 - swift_package( - name = "swiftpkg_starscream", - commit = "c6bfd1af48efcc9a9ad203665db12375ba6b145a", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/daltoniam/Starscream.git", - ) - - # version: 0.4.3 - swift_package( - name = "swiftpkg_subscriptionanalytics_ios", - commit = "53bfc6c6f26322ec647b87c338a071714ac69420", - dependencies_index = "@//:swift_deps_index.json", - remote = "git@bitbucket.org:mobyrix/subscriptionanalytics-ios.git", - ) - - # version: 1.2.3 - swift_package( - name = "swiftpkg_swift_argument_parser", - commit = "41982a3656a71c768319979febd796c6fd111d5c", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/apple/swift-argument-parser", - ) - - # version: 1.1.0 - swift_package( - name = "swiftpkg_swift_collections", - commit = "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/apple/swift-collections", - ) - - # version: 1.0.3 - swift_package( - name = "swiftpkg_swift_http_types", - commit = "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/apple/swift-http-types", - ) - - # version: 1.0.2 - swift_package( - name = "swiftpkg_swift_numerics", - commit = "0a5bc04095a675662cf24757cc0640aa2204253b", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/apple/swift-numerics.git", - ) - - # version: 1.4.0 - swift_package( - name = "swiftpkg_swift_openapi_runtime", - commit = "a51b3bd6f2151e9a6f792ca6937a7242c4758768", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/apple/swift-openapi-runtime", - ) - - # version: 1.0.1 - swift_package( - name = "swiftpkg_swift_openapi_urlsession", - commit = "9229842c63e9fc3bbd32c661d8274b4d9d8715f1", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/apple/swift-openapi-urlsession.git", - ) - - # version: 1.0.3 - swift_package( - name = "swiftpkg_swift_qrcode_generator", - commit = "5ca09b6a2ad190f94aa3d6ddef45b187f8c0343b", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/dagronf/swift-qrcode-generator", - ) - - # version: 1.1.6 - swift_package( - name = "swiftpkg_swiftimagereadwrite", - commit = "5596407d1cf61b953b8e658fa8636a471df3c509", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/dagronf/SwiftImageReadWrite", - ) - - # version: 0.16.4 - swift_package( - name = "swiftpkg_swiftystorekit", - commit = "9ce911639680113dac9b554d6243e406a9758ebe", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/bizz84/SwiftyStoreKit.git", - ) - - # version: 0.2.1 - swift_package( - name = "swiftpkg_tkey_ios", - commit = "c107450f0675351a9a1eaaefe60bcfa285ff1f9e", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/tkey/tkey-ios.git", - ) - - # version: 0.1.5 - swift_package( - name = "swiftpkg_ton_api_swift", - commit = "c1d5a7912480d1794097f4fb8241c3176f394384", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/tonkeeper/ton-api-swift", - ) - - # branch: main - swift_package( - name = "swiftpkg_ton_swift", - commit = "e4c3def222afc125f7ee83c1569004e31f0cd05c", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/denis15yo/ton-swift.git", - ) - - # version: 8.0.1 - swift_package( - name = "swiftpkg_torus_utils_swift", - commit = "608c28404c506983bfec7bbd957632fc0544db8c", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/torusresearch/torus-utils-swift.git", - ) - - # version: 1.1.0 - swift_package( - name = "swiftpkg_tweetnacl_swiftwrap", - commit = "f8fd111642bf2336b11ef9ea828510693106e954", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/bitmark-inc/tweetnacl-swiftwrap", - ) - - # version: 4.0.36 - swift_package( - name = "swiftpkg_wallet_core", - commit = "f14ae4c31e652b293bd5d892b128afc0fa6102c5", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/trustwallet/wallet-core.git", - ) - - # branch: develop - swift_package( - name = "swiftpkg_walletconnectswiftv2", - commit = "1eacd732e321c9511859d7e73303d61d82af4d46", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/denis15yo/WalletConnectSwiftV2.git", - ) - - # version: 2.9.0 - swift_package( - name = "swiftpkg_xcodeedit", - commit = "b6b67389a0f1a6fdd9c6457a8ab5b02eaab13c5c", - dependencies_index = "@//:swift_deps_index.json", - remote = "https://github.com/tomlokhorst/XcodeEdit", - ) diff --git a/swift_deps_index.json b/swift_deps_index.json deleted file mode 100644 index cc95c5c9719..00000000000 --- a/swift_deps_index.json +++ /dev/null @@ -1,2748 +0,0 @@ -{ - "direct_dep_identities": [ - "nicegram-assistant-ios" - ], - "modules": [ - { - "name": "AnyCodable", - "c99name": "AnyCodable", - "src_type": "swift", - "label": "@swiftpkg_anycodable//:AnyCodable.rspm", - "package_identity": "anycodable", - "product_memberships": [ - "AnyCodable" - ] - }, - { - "name": "BigDecimal", - "c99name": "BigDecimal", - "src_type": "swift", - "label": "@swiftpkg_bigdecimal//:BigDecimal.rspm", - "package_identity": "bigdecimal", - "product_memberships": [ - "BigDecimal" - ] - }, - { - "name": "BigInt", - "c99name": "BigInt", - "src_type": "swift", - "label": "@swiftpkg_bigint//:BigInt.rspm", - "package_identity": "bigint", - "product_memberships": [ - "BigInt" - ] - }, - { - "name": "TonConnectAPI", - "c99name": "TonConnectAPI", - "src_type": "swift", - "label": "@swiftpkg_core_swift//:TonConnectAPI.rspm", - "package_identity": "core-swift", - "product_memberships": [ - "TonKeeperWalletCore", - "WalletCoreKeeper" - ] - }, - { - "name": "TonKeeperWalletCore", - "c99name": "TonKeeperWalletCore", - "src_type": "swift", - "label": "@swiftpkg_core_swift//:TonKeeperWalletCore.rspm", - "package_identity": "core-swift", - "product_memberships": [ - "TonKeeperWalletCore" - ] - }, - { - "name": "WalletCoreCore", - "c99name": "WalletCoreCore", - "src_type": "swift", - "label": "@swiftpkg_core_swift//:WalletCoreCore.rspm", - "package_identity": "core-swift", - "product_memberships": [ - "TonKeeperWalletCore", - "WalletCoreCore", - "WalletCoreKeeper" - ] - }, - { - "name": "WalletCoreKeeper", - "c99name": "WalletCoreKeeper", - "src_type": "swift", - "label": "@swiftpkg_core_swift//:WalletCoreKeeper.rspm", - "package_identity": "core-swift", - "product_memberships": [ - "TonKeeperWalletCore", - "WalletCoreKeeper" - ] - }, - { - "name": "CryptoSwift", - "c99name": "CryptoSwift", - "src_type": "swift", - "label": "@swiftpkg_cryptoswift//:CryptoSwift.rspm", - "package_identity": "cryptoswift", - "product_memberships": [ - "CryptoSwift" - ] - }, - { - "name": "curveSecp256k1", - "c99name": "curveSecp256k1", - "src_type": "swift", - "label": "@swiftpkg_curvelib.swift//:curveSecp256k1.rspm", - "package_identity": "curvelib.swift", - "product_memberships": [ - "curveSecp256k1" - ] - }, - { - "name": "curve_secp256k1", - "c99name": "curve_secp256k1", - "src_type": "binary", - "label": "@swiftpkg_curvelib.swift//:curve_secp256k1.rspm", - "package_identity": "curvelib.swift", - "product_memberships": [ - "curveSecp256k1" - ] - }, - { - "name": "curvelib", - "c99name": "curvelib", - "src_type": "clang", - "label": "@swiftpkg_curvelib.swift//:curvelib.rspm", - "package_identity": "curvelib.swift", - "product_memberships": [ - "curveSecp256k1" - ] - }, - { - "name": "Factory", - "c99name": "Factory", - "src_type": "swift", - "label": "@swiftpkg_factory//:Factory.rspm", - "package_identity": "factory", - "product_memberships": [ - "Factory" - ] - }, - { - "name": "FetchNodeDetails", - "c99name": "FetchNodeDetails", - "src_type": "swift", - "label": "@swiftpkg_fetch_node_details_swift//:FetchNodeDetails.rspm", - "package_identity": "fetch-node-details-swift", - "product_memberships": [ - "FetchNodeDetails" - ] - }, - { - "name": "FloatingPanel", - "c99name": "FloatingPanel", - "src_type": "swift", - "label": "@swiftpkg_floatingpanel//:FloatingPanel.rspm", - "modulemap_label": "@swiftpkg_floatingpanel//:FloatingPanel.rspm_modulemap", - "package_identity": "floatingpanel", - "product_memberships": [ - "FloatingPanel" - ] - }, - { - "name": "GRDB", - "c99name": "GRDB", - "src_type": "swift", - "label": "@swiftpkg_grdb.swift//:GRDB.rspm", - "modulemap_label": "@swiftpkg_grdb.swift//:GRDB.rspm_modulemap", - "package_identity": "grdb.swift", - "product_memberships": [ - "GRDB", - "GRDB-dynamic" - ] - }, - { - "name": "KeychainSwift", - "c99name": "KeychainSwift", - "src_type": "swift", - "label": "@swiftpkg_keychain_swift//:KeychainSwift.rspm", - "package_identity": "keychain-swift", - "product_memberships": [ - "KeychainSwift" - ] - }, - { - "name": "LNExtensionExecutor", - "c99name": "LNExtensionExecutor", - "src_type": "objc", - "label": "@swiftpkg_lnextensionexecutor//:LNExtensionExecutor.rspm", - "package_identity": "lnextensionexecutor", - "product_memberships": [ - "LNExtensionExecutor", - "LNExtensionExecutor-Static" - ] - }, - { - "name": "NavigationStackBackport", - "c99name": "NavigationStackBackport", - "src_type": "swift", - "label": "@swiftpkg_navigation_stack_backport//:NavigationStackBackport.rspm", - "package_identity": "navigation-stack-backport", - "product_memberships": [ - "NavigationStackBackport" - ] - }, - { - "name": "CoreSwiftUI", - "c99name": "CoreSwiftUI", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:CoreSwiftUI.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatChatBanner", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPhoneEntryBanner", - "FeatPremiumUI", - "FeatTgChatButton", - "NGAiChatUI", - "NGAssistantUI", - "FeatAuth", - "NGEntryPoint" - ] - }, - { - "name": "FeatAmbassadors", - "c99name": "FeatAmbassadors", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatAmbassadors.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "NGAssistantUI" - ] - }, - { - "name": "FeatAuth", - "c99name": "FeatAuth", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatAuth.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPremiumUI", - "FeatTgChatButton", - "NGAiChatUI", - "NGAssistantUI", - "FeatAuth", - "NGEntryPoint" - ] - }, - { - "name": "FeatAvatarGenerator", - "c99name": "FeatAvatarGenerator", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatAvatarGenerator.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI" - ] - }, - { - "name": "FeatAvatarGeneratorUI", - "c99name": "FeatAvatarGeneratorUI", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatAvatarGeneratorUI.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI" - ] - }, - { - "name": "FeatBilling", - "c99name": "FeatBilling", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatBilling.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPremiumUI", - "FeatSpeechToText", - "FeatTgChatButton", - "NGAiChatUI", - "NGAssistantUI", - "FeatAuth", - "NGEntryPoint" - ] - }, - { - "name": "FeatChatBanner", - "c99name": "FeatChatBanner", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatChatBanner.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatChatBanner" - ] - }, - { - "name": "FeatChatListBanner", - "c99name": "FeatChatListBanner", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatChatListBanner.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "NGAssistantUI" - ] - }, - { - "name": "FeatEsimSplash", - "c99name": "FeatEsimSplash", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatEsimSplash.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatChatBanner" - ] - }, - { - "name": "FeatHiddenChats", - "c99name": "FeatHiddenChats", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatHiddenChats.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatHiddenChats", - "NGAssistantUI" - ] - }, - { - "name": "FeatImagesHub", - "c99name": "FeatImagesHub", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatImagesHub.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatImagesHubUI", - "NGAssistantUI" - ] - }, - { - "name": "FeatImagesHubUI", - "c99name": "FeatImagesHubUI", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatImagesHubUI.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatImagesHubUI", - "NGAssistantUI" - ] - }, - { - "name": "FeatNicegramHub", - "c99name": "FeatNicegramHub", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatNicegramHub.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatNicegramHub", - "NGAssistantUI", - "NGEntryPoint" - ] - }, - { - "name": "FeatOnboarding", - "c99name": "FeatOnboarding", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatOnboarding.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatOnboarding" - ] - }, - { - "name": "FeatPersistentStorage", - "c99name": "FeatPersistentStorage", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatPersistentStorage.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPremiumUI", - "FeatTgChatButton", - "NGAiChat", - "NGAiChatUI", - "NGAssistantUI", - "FeatAuth", - "NGEntryPoint" - ] - }, - { - "name": "FeatPhoneEntryBanner", - "c99name": "FeatPhoneEntryBanner", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatPhoneEntryBanner.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatPhoneEntryBanner" - ] - }, - { - "name": "FeatPinnedChats", - "c99name": "FeatPinnedChats", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatPinnedChats.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatPinnedChats" - ] - }, - { - "name": "FeatPremium", - "c99name": "FeatPremium", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatPremium.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatChatBanner", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPinnedChats", - "FeatPremium", - "FeatPremiumUI", - "FeatSpeechToText", - "FeatTasks", - "FeatTgChatButton", - "NGAiChat", - "NGAiChatUI", - "NGAnalytics", - "NGAssistantUI", - "FeatAuth", - "NGEntryPoint", - "NGRepoUser", - "NGSpecialOffer" - ] - }, - { - "name": "FeatPremiumUI", - "c99name": "FeatPremiumUI", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatPremiumUI.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatImagesHubUI", - "FeatOnboarding", - "FeatPremiumUI", - "NGAssistantUI", - "NGEntryPoint" - ] - }, - { - "name": "FeatRewards", - "c99name": "FeatRewards", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatRewards.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPremiumUI", - "FeatTasks", - "FeatTgChatButton", - "NGAiChatUI", - "NGAssistantUI", - "FeatAuth", - "NGEntryPoint" - ] - }, - { - "name": "FeatRewardsUI", - "c99name": "FeatRewardsUI", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatRewardsUI.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "NGAssistantUI" - ] - }, - { - "name": "FeatSpeechToText", - "c99name": "FeatSpeechToText", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatSpeechToText.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatSpeechToText" - ] - }, - { - "name": "FeatTasks", - "c99name": "FeatTasks", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatTasks.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatTasks", - "NGEntryPoint" - ] - }, - { - "name": "FeatTasksUI", - "c99name": "FeatTasksUI", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatTasksUI.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "NGEntryPoint" - ] - }, - { - "name": "FeatTgChatButton", - "c99name": "FeatTgChatButton", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatTgChatButton.rspm", - "modulemap_label": "@swiftpkg_nicegram_assistant_ios//:FeatTgChatButton.rspm_modulemap", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatTgChatButton" - ] - }, - { - "name": "FeatWallet", - "c99name": "FeatWallet", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatWallet.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatImagesHubUI", - "FeatTgChatButton", - "NGAiChatUI", - "NGAssistantUI" - ] - }, - { - "name": "NGAiChat", - "c99name": "NGAiChat", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:NGAiChat.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPremiumUI", - "FeatTgChatButton", - "NGAiChat", - "NGAiChatUI", - "NGAssistantUI", - "FeatAuth", - "NGEntryPoint" - ] - }, - { - "name": "NGAiChatUI", - "c99name": "NGAiChatUI", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:NGAiChatUI.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatImagesHubUI", - "FeatTgChatButton", - "NGAiChatUI", - "NGAssistantUI" - ] - }, - { - "name": "NGAnalytics", - "c99name": "NGAnalytics", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:NGAnalytics.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatChatBanner", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPinnedChats", - "FeatPremiumUI", - "FeatTasks", - "FeatTgChatButton", - "NGAiChat", - "NGAiChatUI", - "NGAnalytics", - "NGAssistantUI", - "FeatAuth", - "NGEntryPoint", - "NGSpecialOffer" - ] - }, - { - "name": "NGApi", - "c99name": "NGApi", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:NGApi.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatChatBanner", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPinnedChats", - "FeatPremiumUI", - "FeatSpeechToText", - "FeatTasks", - "FeatTgChatButton", - "NGAiChat", - "NGAiChatUI", - "NGAnalytics", - "NGApi", - "NGAssistantUI", - "FeatAuth", - "NGEntryPoint", - "NGRepoUser", - "NGSpecialOffer" - ] - }, - { - "name": "NGAssistant", - "c99name": "NGAssistant", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:NGAssistant.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatImagesHubUI", - "FeatTgChatButton", - "NGAiChatUI", - "NGAssistantUI", - "NGEntryPoint" - ] - }, - { - "name": "NGAssistantUI", - "c99name": "NGAssistantUI", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:NGAssistantUI.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "NGAssistantUI" - ] - }, - { - "name": "NGCore", - "c99name": "NGCore", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:NGCore.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatChatBanner", - "FeatHiddenChats", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPhoneEntryBanner", - "FeatPinnedChats", - "FeatPremium", - "FeatPremiumUI", - "FeatSpeechToText", - "FeatTasks", - "FeatTgChatButton", - "NGAiChat", - "NGAiChatUI", - "NGAnalytics", - "NGApi", - "NGAssistantUI", - "FeatAuth", - "NGCore", - "NGCoreUI", - "NGEntryPoint", - "_NGRemoteConfig", - "NGRepoTg", - "NGRepoUser", - "NGSpecialOffer" - ] - }, - { - "name": "NGCoreUI", - "c99name": "NGCoreUI", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:NGCoreUI.rspm", - "modulemap_label": "@swiftpkg_nicegram_assistant_ios//:NGCoreUI.rspm_modulemap", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatChatBanner", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPhoneEntryBanner", - "FeatPinnedChats", - "FeatPremiumUI", - "FeatTgChatButton", - "NGAiChatUI", - "NGAssistantUI", - "FeatAuth", - "NGCoreUI", - "NGEntryPoint", - "NGSpecialOffer" - ] - }, - { - "name": "NGEntryPoint", - "c99name": "NGEntryPoint", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:NGEntryPoint.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "NGEntryPoint" - ] - }, - { - "name": "NGGrumUI", - "c99name": "NGGrumUI", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:NGGrumUI.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "NGAssistantUI" - ] - }, - { - "name": "NGLocalization", - "c99name": "NGLocalization", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:NGLocalization.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatChatBanner", - "FeatHiddenChats", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPhoneEntryBanner", - "FeatPinnedChats", - "FeatPremium", - "FeatPremiumUI", - "FeatSpeechToText", - "FeatTasks", - "FeatTgChatButton", - "NGAiChat", - "NGAiChatUI", - "NGAnalytics", - "NGApi", - "NGAssistantUI", - "FeatAuth", - "NGCore", - "NGCoreUI", - "NGEntryPoint", - "NGLocalization", - "_NGRemoteConfig", - "NGRepoTg", - "NGRepoUser", - "NGSpecialOffer" - ] - }, - { - "name": "NGRepoTg", - "c99name": "NGRepoTg", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:NGRepoTg.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPremiumUI", - "FeatTgChatButton", - "NGAiChatUI", - "NGAssistantUI", - "FeatAuth", - "NGEntryPoint", - "NGRepoTg" - ] - }, - { - "name": "NGRepoUser", - "c99name": "NGRepoUser", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:NGRepoUser.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatChatBanner", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPinnedChats", - "FeatPremiumUI", - "FeatSpeechToText", - "FeatTasks", - "FeatTgChatButton", - "NGAiChat", - "NGAiChatUI", - "NGAnalytics", - "NGAssistantUI", - "FeatAuth", - "NGEntryPoint", - "NGRepoUser", - "NGSpecialOffer" - ] - }, - { - "name": "NGSpecialOffer", - "c99name": "NGSpecialOffer", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:NGSpecialOffer.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "NGAssistantUI", - "NGSpecialOffer" - ] - }, - { - "name": "Tapjoy", - "c99name": "Tapjoy", - "src_type": "binary", - "label": "@swiftpkg_nicegram_assistant_ios//:Tapjoy.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPremiumUI", - "FeatTasks", - "FeatTgChatButton", - "NGAiChatUI", - "NGAssistantUI", - "FeatAuth", - "NGEntryPoint" - ] - }, - { - "name": "_NGRemoteConfig", - "c99name": "_NGRemoteConfig", - "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:_NGRemoteConfig.rspm", - "package_identity": "nicegram-assistant-ios", - "product_memberships": [ - "FeatAvatarGeneratorUI", - "FeatChatBanner", - "FeatImagesHubUI", - "FeatNicegramHub", - "FeatOnboarding", - "FeatPinnedChats", - "FeatPremiumUI", - "FeatSpeechToText", - "FeatTasks", - "FeatTgChatButton", - "NGAiChat", - "NGAiChatUI", - "NGAssistantUI", - "FeatAuth", - "NGEntryPoint", - "_NGRemoteConfig", - "NGSpecialOffer" - ] - }, - { - "name": "NicegramWallet", - "c99name": "NicegramWallet", - "src_type": "swift", - "label": "@swiftpkg_nicegram_wallet_ios//:NicegramWallet.rspm", - "modulemap_label": "@swiftpkg_nicegram_wallet_ios//:NicegramWallet.rspm_modulemap", - "package_identity": "nicegram-wallet-ios", - "product_memberships": [ - "NicegramWallet" - ] - }, - { - "name": "QRCode", - "c99name": "QRCode", - "src_type": "swift", - "label": "@swiftpkg_qrcode//:QRCode.rspm", - "modulemap_label": "@swiftpkg_qrcode//:QRCode.rspm_modulemap", - "package_identity": "qrcode", - "product_memberships": [ - "QRCode", - "QRCodeStatic", - "QRCodeDynamic" - ] - }, - { - "name": "RswiftGenerateInternalResources", - "c99name": "RswiftGenerateInternalResources", - "src_type": "unknown", - "label": "@swiftpkg_r.swift//:RswiftGenerateInternalResources.rspm", - "package_identity": "r.swift", - "product_memberships": [ - "RswiftGenerateInternalResources" - ] - }, - { - "name": "RswiftGeneratePublicResources", - "c99name": "RswiftGeneratePublicResources", - "src_type": "unknown", - "label": "@swiftpkg_r.swift//:RswiftGeneratePublicResources.rspm", - "package_identity": "r.swift", - "product_memberships": [ - "RswiftGeneratePublicResources" - ] - }, - { - "name": "RswiftGenerateResourcesCommand", - "c99name": "RswiftGenerateResourcesCommand", - "src_type": "unknown", - "label": "@swiftpkg_r.swift//:RswiftGenerateResourcesCommand.rspm", - "package_identity": "r.swift", - "product_memberships": [ - "RswiftGenerateResourcesCommand" - ] - }, - { - "name": "RswiftGenerators", - "c99name": "RswiftGenerators", - "src_type": "swift", - "label": "@swiftpkg_r.swift//:RswiftGenerators.rspm", - "package_identity": "r.swift", - "product_memberships": [ - "rswift", - "RswiftGenerateInternalResources", - "RswiftGeneratePublicResources", - "RswiftGenerateResourcesCommand", - "RswiftModifyXcodePackages" - ] - }, - { - "name": "RswiftModifyXcodePackages", - "c99name": "RswiftModifyXcodePackages", - "src_type": "unknown", - "label": "@swiftpkg_r.swift//:RswiftModifyXcodePackages.rspm", - "package_identity": "r.swift", - "product_memberships": [ - "RswiftModifyXcodePackages" - ] - }, - { - "name": "RswiftParsers", - "c99name": "RswiftParsers", - "src_type": "swift", - "label": "@swiftpkg_r.swift//:RswiftParsers.rspm", - "modulemap_label": "@swiftpkg_r.swift//:RswiftParsers.rspm_modulemap", - "package_identity": "r.swift", - "product_memberships": [ - "rswift", - "RswiftGenerateInternalResources", - "RswiftGeneratePublicResources", - "RswiftGenerateResourcesCommand", - "RswiftModifyXcodePackages" - ] - }, - { - "name": "RswiftResources", - "c99name": "RswiftResources", - "src_type": "swift", - "label": "@swiftpkg_r.swift//:RswiftResources.rspm", - "package_identity": "r.swift", - "product_memberships": [ - "rswift", - "RswiftLibrary", - "RswiftGenerateInternalResources", - "RswiftGeneratePublicResources", - "RswiftGenerateResourcesCommand", - "RswiftModifyXcodePackages" - ] - }, - { - "name": "rswift", - "c99name": "rswift", - "src_type": "swift", - "label": "@swiftpkg_r.swift//:rswift.rspm", - "package_identity": "r.swift", - "product_memberships": [ - "rswift", - "RswiftGenerateInternalResources", - "RswiftGeneratePublicResources", - "RswiftGenerateResourcesCommand", - "RswiftModifyXcodePackages" - ] - }, - { - "name": "SDWebImage", - "c99name": "SDWebImage", - "src_type": "objc", - "label": "@swiftpkg_sdwebimage//:SDWebImage.rspm", - "package_identity": "sdwebimage", - "product_memberships": [ - "SDWebImage", - "SDWebImageMapKit" - ] - }, - { - "name": "SDWebImageMapKit", - "c99name": "SDWebImageMapKit", - "src_type": "objc", - "label": "@swiftpkg_sdwebimage//:SDWebImageMapKit.rspm", - "package_identity": "sdwebimage", - "product_memberships": [ - "SDWebImageMapKit" - ] - }, - { - "name": "SessionManager", - "c99name": "SessionManager", - "src_type": "swift", - "label": "@swiftpkg_session_manager_swift//:SessionManager.rspm", - "package_identity": "session-manager-swift", - "product_memberships": [ - "SessionManager" - ] - }, - { - "name": "SingleFactorAuth", - "c99name": "SingleFactorAuth", - "src_type": "swift", - "label": "@swiftpkg_single_factor_auth_swift//:SingleFactorAuth.rspm", - "package_identity": "single-factor-auth-swift", - "product_memberships": [ - "SingleFactorAuth" - ] - }, - { - "name": "SnapKit", - "c99name": "SnapKit", - "src_type": "swift", - "label": "@swiftpkg_snapkit//:SnapKit.rspm", - "package_identity": "snapkit", - "product_memberships": [ - "SnapKit", - "SnapKit-Dynamic" - ] - }, - { - "name": "Starscream", - "c99name": "Starscream", - "src_type": "swift", - "label": "@swiftpkg_starscream//:Starscream.rspm", - "package_identity": "starscream", - "product_memberships": [ - "Starscream" - ] - }, - { - "name": "SubscriptionAnalytics", - "c99name": "SubscriptionAnalytics", - "src_type": "swift", - "label": "@swiftpkg_subscriptionanalytics_ios//:SubscriptionAnalytics.rspm", - "package_identity": "subscriptionanalytics-ios", - "product_memberships": [ - "SubscriptionAnalytics" - ] - }, - { - "name": "ArgumentParser", - "c99name": "ArgumentParser", - "src_type": "swift", - "label": "@swiftpkg_swift_argument_parser//:ArgumentParser.rspm", - "package_identity": "swift-argument-parser", - "product_memberships": [ - "ArgumentParser", - "GenerateManual" - ] - }, - { - "name": "ArgumentParserToolInfo", - "c99name": "ArgumentParserToolInfo", - "src_type": "swift", - "label": "@swiftpkg_swift_argument_parser//:ArgumentParserToolInfo.rspm", - "package_identity": "swift-argument-parser", - "product_memberships": [ - "ArgumentParser", - "GenerateManual" - ] - }, - { - "name": "GenerateManual", - "c99name": "GenerateManual", - "src_type": "unknown", - "label": "@swiftpkg_swift_argument_parser//:GenerateManual.rspm", - "package_identity": "swift-argument-parser", - "product_memberships": [ - "GenerateManual" - ] - }, - { - "name": "generate-manual", - "c99name": "generate_manual", - "src_type": "swift", - "label": "@swiftpkg_swift_argument_parser//:generate-manual.rspm", - "package_identity": "swift-argument-parser", - "product_memberships": [ - "GenerateManual" - ] - }, - { - "name": "BitCollections", - "c99name": "BitCollections", - "src_type": "swift", - "label": "@swiftpkg_swift_collections//:BitCollections.rspm", - "package_identity": "swift-collections", - "product_memberships": [ - "BitCollections", - "Collections" - ] - }, - { - "name": "Collections", - "c99name": "Collections", - "src_type": "swift", - "label": "@swiftpkg_swift_collections//:Collections.rspm", - "package_identity": "swift-collections", - "product_memberships": [ - "Collections" - ] - }, - { - "name": "DequeModule", - "c99name": "DequeModule", - "src_type": "swift", - "label": "@swiftpkg_swift_collections//:DequeModule.rspm", - "package_identity": "swift-collections", - "product_memberships": [ - "DequeModule", - "Collections" - ] - }, - { - "name": "HashTreeCollections", - "c99name": "HashTreeCollections", - "src_type": "swift", - "label": "@swiftpkg_swift_collections//:HashTreeCollections.rspm", - "package_identity": "swift-collections", - "product_memberships": [ - "HashTreeCollections", - "Collections" - ] - }, - { - "name": "HeapModule", - "c99name": "HeapModule", - "src_type": "swift", - "label": "@swiftpkg_swift_collections//:HeapModule.rspm", - "package_identity": "swift-collections", - "product_memberships": [ - "HeapModule", - "Collections" - ] - }, - { - "name": "InternalCollectionsUtilities", - "c99name": "InternalCollectionsUtilities", - "src_type": "swift", - "label": "@swiftpkg_swift_collections//:InternalCollectionsUtilities.rspm", - "package_identity": "swift-collections", - "product_memberships": [ - "BitCollections", - "DequeModule", - "HashTreeCollections", - "HeapModule", - "OrderedCollections", - "_RopeModule", - "Collections" - ] - }, - { - "name": "OrderedCollections", - "c99name": "OrderedCollections", - "src_type": "swift", - "label": "@swiftpkg_swift_collections//:OrderedCollections.rspm", - "package_identity": "swift-collections", - "product_memberships": [ - "OrderedCollections", - "Collections" - ] - }, - { - "name": "_RopeModule", - "c99name": "_RopeModule", - "src_type": "swift", - "label": "@swiftpkg_swift_collections//:_RopeModule.rspm", - "package_identity": "swift-collections", - "product_memberships": [ - "_RopeModule", - "Collections" - ] - }, - { - "name": "HTTPTypes", - "c99name": "HTTPTypes", - "src_type": "swift", - "label": "@swiftpkg_swift_http_types//:HTTPTypes.rspm", - "package_identity": "swift-http-types", - "product_memberships": [ - "HTTPTypes", - "HTTPTypesFoundation" - ] - }, - { - "name": "HTTPTypesFoundation", - "c99name": "HTTPTypesFoundation", - "src_type": "swift", - "label": "@swiftpkg_swift_http_types//:HTTPTypesFoundation.rspm", - "package_identity": "swift-http-types", - "product_memberships": [ - "HTTPTypesFoundation" - ] - }, - { - "name": "ComplexModule", - "c99name": "ComplexModule", - "src_type": "swift", - "label": "@swiftpkg_swift_numerics//:ComplexModule.rspm", - "package_identity": "swift-numerics", - "product_memberships": [ - "ComplexModule", - "Numerics" - ] - }, - { - "name": "Numerics", - "c99name": "Numerics", - "src_type": "swift", - "label": "@swiftpkg_swift_numerics//:Numerics.rspm", - "package_identity": "swift-numerics", - "product_memberships": [ - "Numerics" - ] - }, - { - "name": "RealModule", - "c99name": "RealModule", - "src_type": "swift", - "label": "@swiftpkg_swift_numerics//:RealModule.rspm", - "package_identity": "swift-numerics", - "product_memberships": [ - "ComplexModule", - "Numerics", - "RealModule" - ] - }, - { - "name": "_NumericsShims", - "c99name": "_NumericsShims", - "src_type": "clang", - "label": "@swiftpkg_swift_numerics//:_NumericsShims.rspm", - "package_identity": "swift-numerics", - "product_memberships": [ - "ComplexModule", - "Numerics", - "RealModule" - ] - }, - { - "name": "OpenAPIRuntime", - "c99name": "OpenAPIRuntime", - "src_type": "swift", - "label": "@swiftpkg_swift_openapi_runtime//:OpenAPIRuntime.rspm", - "package_identity": "swift-openapi-runtime", - "product_memberships": [ - "OpenAPIRuntime" - ] - }, - { - "name": "OpenAPIURLSession", - "c99name": "OpenAPIURLSession", - "src_type": "swift", - "label": "@swiftpkg_swift_openapi_urlsession//:OpenAPIURLSession.rspm", - "package_identity": "swift-openapi-urlsession", - "product_memberships": [ - "OpenAPIURLSession" - ] - }, - { - "name": "QRCodeGenerator", - "c99name": "QRCodeGenerator", - "src_type": "swift", - "label": "@swiftpkg_swift_qrcode_generator//:QRCodeGenerator.rspm", - "package_identity": "swift-qrcode-generator", - "product_memberships": [ - "QRCodeGenerator" - ] - }, - { - "name": "SwiftImageReadWrite", - "c99name": "SwiftImageReadWrite", - "src_type": "swift", - "label": "@swiftpkg_swiftimagereadwrite//:SwiftImageReadWrite.rspm", - "package_identity": "swiftimagereadwrite", - "product_memberships": [ - "SwiftImageReadWrite" - ] - }, - { - "name": "SwiftyStoreKit", - "c99name": "SwiftyStoreKit", - "src_type": "swift", - "label": "@swiftpkg_swiftystorekit//:SwiftyStoreKit.rspm", - "package_identity": "swiftystorekit", - "product_memberships": [ - "SwiftyStoreKit" - ] - }, - { - "name": "lib", - "c99name": "lib", - "src_type": "clang", - "label": "@swiftpkg_tkey_ios//:lib.rspm", - "package_identity": "tkey-ios", - "product_memberships": [ - "tkey-swift" - ] - }, - { - "name": "libtkey", - "c99name": "libtkey", - "src_type": "binary", - "label": "@swiftpkg_tkey_ios//:libtkey.rspm", - "package_identity": "tkey-ios", - "product_memberships": [ - "tkey-swift" - ] - }, - { - "name": "tkey-swift", - "c99name": "tkey_swift", - "src_type": "swift", - "label": "@swiftpkg_tkey_ios//:tkey-swift.rspm", - "package_identity": "tkey-ios", - "product_memberships": [ - "tkey-swift" - ] - }, - { - "name": "EventSource", - "c99name": "EventSource", - "src_type": "swift", - "label": "@swiftpkg_ton_api_swift//:EventSource.rspm", - "package_identity": "ton-api-swift", - "product_memberships": [ - "EventSource" - ] - }, - { - "name": "StreamURLSessionTransport", - "c99name": "StreamURLSessionTransport", - "src_type": "swift", - "label": "@swiftpkg_ton_api_swift//:StreamURLSessionTransport.rspm", - "package_identity": "ton-api-swift", - "product_memberships": [ - "StreamURLSessionTransport" - ] - }, - { - "name": "TonAPI", - "c99name": "TonAPI", - "src_type": "swift", - "label": "@swiftpkg_ton_api_swift//:TonAPI.rspm", - "package_identity": "ton-api-swift", - "product_memberships": [ - "TonAPI" - ] - }, - { - "name": "TonStreamingAPI", - "c99name": "TonStreamingAPI", - "src_type": "swift", - "label": "@swiftpkg_ton_api_swift//:TonStreamingAPI.rspm", - "package_identity": "ton-api-swift", - "product_memberships": [ - "TonStreamingAPI" - ] - }, - { - "name": "TonSwift", - "c99name": "TonSwift", - "src_type": "swift", - "label": "@swiftpkg_ton_swift//:TonSwift.rspm", - "package_identity": "ton-swift", - "product_memberships": [ - "TonSwift" - ] - }, - { - "name": "TorusUtils", - "c99name": "TorusUtils", - "src_type": "swift", - "label": "@swiftpkg_torus_utils_swift//:TorusUtils.rspm", - "package_identity": "torus-utils-swift", - "product_memberships": [ - "TorusUtils" - ] - }, - { - "name": "CTweetNacl", - "c99name": "CTweetNacl", - "src_type": "clang", - "label": "@swiftpkg_tweetnacl_swiftwrap//:CTweetNacl.rspm", - "package_identity": "tweetnacl-swiftwrap", - "product_memberships": [ - "TweetNacl" - ] - }, - { - "name": "TweetNacl", - "c99name": "TweetNacl", - "src_type": "swift", - "label": "@swiftpkg_tweetnacl_swiftwrap//:TweetNacl.rspm", - "package_identity": "tweetnacl-swiftwrap", - "product_memberships": [ - "TweetNacl" - ] - }, - { - "name": "SwiftProtobuf", - "c99name": "SwiftProtobuf", - "src_type": "binary", - "label": "@swiftpkg_wallet_core//:SwiftProtobuf.rspm", - "package_identity": "wallet-core", - "product_memberships": [ - "SwiftProtobuf" - ] - }, - { - "name": "WalletCore", - "c99name": "WalletCore", - "src_type": "binary", - "label": "@swiftpkg_wallet_core//:WalletCore.rspm", - "package_identity": "wallet-core", - "product_memberships": [ - "WalletCore" - ] - }, - { - "name": "Auth", - "c99name": "Auth", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:Auth.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnectAuth" - ] - }, - { - "name": "Commons", - "c99name": "Commons", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:Commons.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnect", - "WalletConnectAuth", - "Web3Wallet", - "WalletConnectPairing", - "WalletConnectNotify", - "WalletConnectPush", - "WalletConnectNetworking", - "WalletConnectVerify", - "WalletConnectModal", - "WalletConnectIdentity" - ] - }, - { - "name": "Database", - "c99name": "Database", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:Database.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnectNotify" - ] - }, - { - "name": "HTTPClient", - "c99name": "HTTPClient", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:HTTPClient.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnect", - "WalletConnectAuth", - "Web3Wallet", - "WalletConnectPairing", - "WalletConnectNotify", - "WalletConnectPush", - "WalletConnectNetworking", - "WalletConnectVerify", - "WalletConnectModal", - "WalletConnectIdentity" - ] - }, - { - "name": "JSONRPC", - "c99name": "JSONRPC", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:JSONRPC.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnect", - "WalletConnectAuth", - "Web3Wallet", - "WalletConnectPairing", - "WalletConnectNotify", - "WalletConnectPush", - "WalletConnectNetworking", - "WalletConnectVerify", - "WalletConnectModal", - "WalletConnectIdentity" - ] - }, - { - "name": "WalletConnectIdentity", - "c99name": "WalletConnectIdentity", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectIdentity.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnectNotify", - "WalletConnectIdentity" - ] - }, - { - "name": "WalletConnectJWT", - "c99name": "WalletConnectJWT", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectJWT.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnect", - "WalletConnectAuth", - "Web3Wallet", - "WalletConnectPairing", - "WalletConnectNotify", - "WalletConnectPush", - "WalletConnectNetworking", - "WalletConnectVerify", - "WalletConnectModal", - "WalletConnectIdentity" - ] - }, - { - "name": "WalletConnectKMS", - "c99name": "WalletConnectKMS", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectKMS.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnect", - "WalletConnectAuth", - "Web3Wallet", - "WalletConnectPairing", - "WalletConnectNotify", - "WalletConnectPush", - "WalletConnectNetworking", - "WalletConnectVerify", - "WalletConnectModal", - "WalletConnectIdentity" - ] - }, - { - "name": "WalletConnectModal", - "c99name": "WalletConnectModal", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectModal.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnectModal" - ] - }, - { - "name": "WalletConnectNetworking", - "c99name": "WalletConnectNetworking", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectNetworking.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnect", - "WalletConnectAuth", - "Web3Wallet", - "WalletConnectPairing", - "WalletConnectNotify", - "WalletConnectPush", - "WalletConnectNetworking", - "WalletConnectVerify", - "WalletConnectModal", - "WalletConnectIdentity" - ] - }, - { - "name": "WalletConnectNotify", - "c99name": "WalletConnectNotify", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectNotify.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnectNotify" - ] - }, - { - "name": "WalletConnectPairing", - "c99name": "WalletConnectPairing", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectPairing.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnect", - "WalletConnectAuth", - "Web3Wallet", - "WalletConnectPairing", - "WalletConnectNotify", - "WalletConnectModal" - ] - }, - { - "name": "WalletConnectPush", - "c99name": "WalletConnectPush", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectPush.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "Web3Wallet", - "WalletConnectNotify", - "WalletConnectPush" - ] - }, - { - "name": "WalletConnectRelay", - "c99name": "WalletConnectRelay", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectRelay.rspm", - "modulemap_label": "@swiftpkg_walletconnectswiftv2//:WalletConnectRelay.rspm_modulemap", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnect", - "WalletConnectAuth", - "Web3Wallet", - "WalletConnectPairing", - "WalletConnectNotify", - "WalletConnectPush", - "WalletConnectNetworking", - "WalletConnectVerify", - "WalletConnectModal", - "WalletConnectIdentity" - ] - }, - { - "name": "WalletConnectRouter", - "c99name": "WalletConnectRouter", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectRouter.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnectRouter" - ] - }, - { - "name": "WalletConnectRouterLegacy", - "c99name": "WalletConnectRouterLegacy", - "src_type": "objc", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectRouterLegacy.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnectRouter" - ] - }, - { - "name": "WalletConnectSign", - "c99name": "WalletConnectSign", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectSign.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnect", - "Web3Wallet", - "WalletConnectModal" - ] - }, - { - "name": "WalletConnectSigner", - "c99name": "WalletConnectSigner", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectSigner.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnect", - "WalletConnectAuth", - "Web3Wallet", - "WalletConnectNotify", - "WalletConnectModal" - ] - }, - { - "name": "WalletConnectUtils", - "c99name": "WalletConnectUtils", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectUtils.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnect", - "WalletConnectAuth", - "Web3Wallet", - "WalletConnectPairing", - "WalletConnectNotify", - "WalletConnectPush", - "WalletConnectNetworking", - "WalletConnectVerify", - "WalletConnectModal", - "WalletConnectIdentity" - ] - }, - { - "name": "WalletConnectVerify", - "c99name": "WalletConnectVerify", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectVerify.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "WalletConnect", - "WalletConnectAuth", - "Web3Wallet", - "WalletConnectVerify", - "WalletConnectModal" - ] - }, - { - "name": "Web3Wallet", - "c99name": "Web3Wallet", - "src_type": "swift", - "label": "@swiftpkg_walletconnectswiftv2//:Web3Wallet.rspm", - "package_identity": "walletconnectswiftv2", - "product_memberships": [ - "Web3Wallet" - ] - }, - { - "name": "XcodeEdit", - "c99name": "XcodeEdit", - "src_type": "swift", - "label": "@swiftpkg_xcodeedit//:XcodeEdit.rspm", - "package_identity": "xcodeedit", - "product_memberships": [ - "XcodeEdit" - ] - } - ], - "products": [ - { - "identity": "anycodable", - "name": "AnyCodable", - "type": "library", - "label": "@swiftpkg_anycodable//:AnyCodable" - }, - { - "identity": "bigdecimal", - "name": "BigDecimal", - "type": "library", - "label": "@swiftpkg_bigdecimal//:BigDecimal" - }, - { - "identity": "bigint", - "name": "BigInt", - "type": "library", - "label": "@swiftpkg_bigint//:BigInt" - }, - { - "identity": "core-swift", - "name": "TonKeeperWalletCore", - "type": "library", - "label": "@swiftpkg_core_swift//:TonKeeperWalletCore" - }, - { - "identity": "core-swift", - "name": "WalletCoreCore", - "type": "library", - "label": "@swiftpkg_core_swift//:WalletCoreCore" - }, - { - "identity": "core-swift", - "name": "WalletCoreKeeper", - "type": "library", - "label": "@swiftpkg_core_swift//:WalletCoreKeeper" - }, - { - "identity": "cryptoswift", - "name": "CryptoSwift", - "type": "library", - "label": "@swiftpkg_cryptoswift//:CryptoSwift" - }, - { - "identity": "curvelib.swift", - "name": "curveSecp256k1", - "type": "library", - "label": "@swiftpkg_curvelib.swift//:curveSecp256k1" - }, - { - "identity": "factory", - "name": "Factory", - "type": "library", - "label": "@swiftpkg_factory//:Factory" - }, - { - "identity": "fetch-node-details-swift", - "name": "FetchNodeDetails", - "type": "library", - "label": "@swiftpkg_fetch_node_details_swift//:FetchNodeDetails" - }, - { - "identity": "floatingpanel", - "name": "FloatingPanel", - "type": "library", - "label": "@swiftpkg_floatingpanel//:FloatingPanel" - }, - { - "identity": "grdb.swift", - "name": "GRDB", - "type": "library", - "label": "@swiftpkg_grdb.swift//:GRDB" - }, - { - "identity": "grdb.swift", - "name": "GRDB-dynamic", - "type": "library", - "label": "@swiftpkg_grdb.swift//:GRDB-dynamic" - }, - { - "identity": "keychain-swift", - "name": "KeychainSwift", - "type": "library", - "label": "@swiftpkg_keychain_swift//:KeychainSwift" - }, - { - "identity": "lnextensionexecutor", - "name": "LNExtensionExecutor", - "type": "library", - "label": "@swiftpkg_lnextensionexecutor//:LNExtensionExecutor" - }, - { - "identity": "lnextensionexecutor", - "name": "LNExtensionExecutor-Static", - "type": "library", - "label": "@swiftpkg_lnextensionexecutor//:LNExtensionExecutor-Static" - }, - { - "identity": "navigation-stack-backport", - "name": "NavigationStackBackport", - "type": "library", - "label": "@swiftpkg_navigation_stack_backport//:NavigationStackBackport" - }, - { - "identity": "nicegram-assistant-ios", - "name": "FeatAuth", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatAuth" - }, - { - "identity": "nicegram-assistant-ios", - "name": "FeatAvatarGeneratorUI", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatAvatarGeneratorUI" - }, - { - "identity": "nicegram-assistant-ios", - "name": "FeatChatBanner", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatChatBanner" - }, - { - "identity": "nicegram-assistant-ios", - "name": "FeatHiddenChats", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatHiddenChats" - }, - { - "identity": "nicegram-assistant-ios", - "name": "FeatImagesHubUI", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatImagesHubUI" - }, - { - "identity": "nicegram-assistant-ios", - "name": "FeatNicegramHub", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatNicegramHub" - }, - { - "identity": "nicegram-assistant-ios", - "name": "FeatOnboarding", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatOnboarding" - }, - { - "identity": "nicegram-assistant-ios", - "name": "FeatPhoneEntryBanner", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatPhoneEntryBanner" - }, - { - "identity": "nicegram-assistant-ios", - "name": "FeatPinnedChats", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatPinnedChats" - }, - { - "identity": "nicegram-assistant-ios", - "name": "FeatPremium", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatPremium" - }, - { - "identity": "nicegram-assistant-ios", - "name": "FeatPremiumUI", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatPremiumUI" - }, - { - "identity": "nicegram-assistant-ios", - "name": "FeatSpeechToText", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatSpeechToText" - }, - { - "identity": "nicegram-assistant-ios", - "name": "FeatTasks", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatTasks" - }, - { - "identity": "nicegram-assistant-ios", - "name": "FeatTgChatButton", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:FeatTgChatButton" - }, - { - "identity": "nicegram-assistant-ios", - "name": "NGAiChat", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:NGAiChat" - }, - { - "identity": "nicegram-assistant-ios", - "name": "NGAiChatUI", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:NGAiChatUI" - }, - { - "identity": "nicegram-assistant-ios", - "name": "NGAnalytics", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:NGAnalytics" - }, - { - "identity": "nicegram-assistant-ios", - "name": "NGApi", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:NGApi" - }, - { - "identity": "nicegram-assistant-ios", - "name": "NGAssistantUI", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:NGAssistantUI" - }, - { - "identity": "nicegram-assistant-ios", - "name": "NGCore", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:NGCore" - }, - { - "identity": "nicegram-assistant-ios", - "name": "NGCoreUI", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:NGCoreUI" - }, - { - "identity": "nicegram-assistant-ios", - "name": "NGEntryPoint", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:NGEntryPoint" - }, - { - "identity": "nicegram-assistant-ios", - "name": "NGLocalization", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:NGLocalization" - }, - { - "identity": "nicegram-assistant-ios", - "name": "NGRepoTg", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:NGRepoTg" - }, - { - "identity": "nicegram-assistant-ios", - "name": "NGRepoUser", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:NGRepoUser" - }, - { - "identity": "nicegram-assistant-ios", - "name": "NGSpecialOffer", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:NGSpecialOffer" - }, - { - "identity": "nicegram-assistant-ios", - "name": "_NGRemoteConfig", - "type": "library", - "label": "@swiftpkg_nicegram_assistant_ios//:_NGRemoteConfig" - }, - { - "identity": "nicegram-wallet-ios", - "name": "NicegramWallet", - "type": "library", - "label": "@swiftpkg_nicegram_wallet_ios//:NicegramWallet" - }, - { - "identity": "qrcode", - "name": "QRCode", - "type": "library", - "label": "@swiftpkg_qrcode//:QRCode" - }, - { - "identity": "qrcode", - "name": "QRCodeDynamic", - "type": "library", - "label": "@swiftpkg_qrcode//:QRCodeDynamic" - }, - { - "identity": "qrcode", - "name": "QRCodeStatic", - "type": "library", - "label": "@swiftpkg_qrcode//:QRCodeStatic" - }, - { - "identity": "r.swift", - "name": "RswiftGenerateInternalResources", - "type": "plugin", - "label": "@swiftpkg_r.swift//:RswiftGenerateInternalResources" - }, - { - "identity": "r.swift", - "name": "RswiftGeneratePublicResources", - "type": "plugin", - "label": "@swiftpkg_r.swift//:RswiftGeneratePublicResources" - }, - { - "identity": "r.swift", - "name": "RswiftGenerateResourcesCommand", - "type": "plugin", - "label": "@swiftpkg_r.swift//:RswiftGenerateResourcesCommand" - }, - { - "identity": "r.swift", - "name": "RswiftLibrary", - "type": "library", - "label": "@swiftpkg_r.swift//:RswiftLibrary" - }, - { - "identity": "r.swift", - "name": "RswiftModifyXcodePackages", - "type": "plugin", - "label": "@swiftpkg_r.swift//:RswiftModifyXcodePackages" - }, - { - "identity": "r.swift", - "name": "rswift", - "type": "executable", - "label": "@swiftpkg_r.swift//:rswift" - }, - { - "identity": "sdwebimage", - "name": "SDWebImage", - "type": "library", - "label": "@swiftpkg_sdwebimage//:SDWebImage" - }, - { - "identity": "sdwebimage", - "name": "SDWebImageMapKit", - "type": "library", - "label": "@swiftpkg_sdwebimage//:SDWebImageMapKit" - }, - { - "identity": "session-manager-swift", - "name": "SessionManager", - "type": "library", - "label": "@swiftpkg_session_manager_swift//:SessionManager" - }, - { - "identity": "single-factor-auth-swift", - "name": "SingleFactorAuth", - "type": "library", - "label": "@swiftpkg_single_factor_auth_swift//:SingleFactorAuth" - }, - { - "identity": "snapkit", - "name": "SnapKit", - "type": "library", - "label": "@swiftpkg_snapkit//:SnapKit" - }, - { - "identity": "snapkit", - "name": "SnapKit-Dynamic", - "type": "library", - "label": "@swiftpkg_snapkit//:SnapKit-Dynamic" - }, - { - "identity": "starscream", - "name": "Starscream", - "type": "library", - "label": "@swiftpkg_starscream//:Starscream" - }, - { - "identity": "subscriptionanalytics-ios", - "name": "SubscriptionAnalytics", - "type": "library", - "label": "@swiftpkg_subscriptionanalytics_ios//:SubscriptionAnalytics" - }, - { - "identity": "swift-argument-parser", - "name": "ArgumentParser", - "type": "library", - "label": "@swiftpkg_swift_argument_parser//:ArgumentParser" - }, - { - "identity": "swift-argument-parser", - "name": "GenerateManual", - "type": "plugin", - "label": "@swiftpkg_swift_argument_parser//:GenerateManual" - }, - { - "identity": "swift-collections", - "name": "BitCollections", - "type": "library", - "label": "@swiftpkg_swift_collections//:BitCollections" - }, - { - "identity": "swift-collections", - "name": "Collections", - "type": "library", - "label": "@swiftpkg_swift_collections//:Collections" - }, - { - "identity": "swift-collections", - "name": "DequeModule", - "type": "library", - "label": "@swiftpkg_swift_collections//:DequeModule" - }, - { - "identity": "swift-collections", - "name": "HashTreeCollections", - "type": "library", - "label": "@swiftpkg_swift_collections//:HashTreeCollections" - }, - { - "identity": "swift-collections", - "name": "HeapModule", - "type": "library", - "label": "@swiftpkg_swift_collections//:HeapModule" - }, - { - "identity": "swift-collections", - "name": "OrderedCollections", - "type": "library", - "label": "@swiftpkg_swift_collections//:OrderedCollections" - }, - { - "identity": "swift-collections", - "name": "_RopeModule", - "type": "library", - "label": "@swiftpkg_swift_collections//:_RopeModule" - }, - { - "identity": "swift-http-types", - "name": "HTTPTypes", - "type": "library", - "label": "@swiftpkg_swift_http_types//:HTTPTypes" - }, - { - "identity": "swift-http-types", - "name": "HTTPTypesFoundation", - "type": "library", - "label": "@swiftpkg_swift_http_types//:HTTPTypesFoundation" - }, - { - "identity": "swift-numerics", - "name": "ComplexModule", - "type": "library", - "label": "@swiftpkg_swift_numerics//:ComplexModule" - }, - { - "identity": "swift-numerics", - "name": "Numerics", - "type": "library", - "label": "@swiftpkg_swift_numerics//:Numerics" - }, - { - "identity": "swift-numerics", - "name": "RealModule", - "type": "library", - "label": "@swiftpkg_swift_numerics//:RealModule" - }, - { - "identity": "swift-openapi-runtime", - "name": "OpenAPIRuntime", - "type": "library", - "label": "@swiftpkg_swift_openapi_runtime//:OpenAPIRuntime" - }, - { - "identity": "swift-openapi-urlsession", - "name": "OpenAPIURLSession", - "type": "library", - "label": "@swiftpkg_swift_openapi_urlsession//:OpenAPIURLSession" - }, - { - "identity": "swift-qrcode-generator", - "name": "QRCodeGenerator", - "type": "library", - "label": "@swiftpkg_swift_qrcode_generator//:QRCodeGenerator" - }, - { - "identity": "swiftimagereadwrite", - "name": "SwiftImageReadWrite", - "type": "library", - "label": "@swiftpkg_swiftimagereadwrite//:SwiftImageReadWrite" - }, - { - "identity": "swiftystorekit", - "name": "SwiftyStoreKit", - "type": "library", - "label": "@swiftpkg_swiftystorekit//:SwiftyStoreKit" - }, - { - "identity": "tkey-ios", - "name": "tkey-swift", - "type": "library", - "label": "@swiftpkg_tkey_ios//:tkey-swift" - }, - { - "identity": "ton-api-swift", - "name": "EventSource", - "type": "library", - "label": "@swiftpkg_ton_api_swift//:EventSource" - }, - { - "identity": "ton-api-swift", - "name": "StreamURLSessionTransport", - "type": "library", - "label": "@swiftpkg_ton_api_swift//:StreamURLSessionTransport" - }, - { - "identity": "ton-api-swift", - "name": "TonAPI", - "type": "library", - "label": "@swiftpkg_ton_api_swift//:TonAPI" - }, - { - "identity": "ton-api-swift", - "name": "TonStreamingAPI", - "type": "library", - "label": "@swiftpkg_ton_api_swift//:TonStreamingAPI" - }, - { - "identity": "ton-swift", - "name": "TonSwift", - "type": "library", - "label": "@swiftpkg_ton_swift//:TonSwift" - }, - { - "identity": "torus-utils-swift", - "name": "TorusUtils", - "type": "library", - "label": "@swiftpkg_torus_utils_swift//:TorusUtils" - }, - { - "identity": "tweetnacl-swiftwrap", - "name": "TweetNacl", - "type": "library", - "label": "@swiftpkg_tweetnacl_swiftwrap//:TweetNacl" - }, - { - "identity": "wallet-core", - "name": "SwiftProtobuf", - "type": "library", - "label": "@swiftpkg_wallet_core//:SwiftProtobuf" - }, - { - "identity": "wallet-core", - "name": "WalletCore", - "type": "library", - "label": "@swiftpkg_wallet_core//:WalletCore" - }, - { - "identity": "walletconnectswiftv2", - "name": "WalletConnect", - "type": "library", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnect" - }, - { - "identity": "walletconnectswiftv2", - "name": "WalletConnectAuth", - "type": "library", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectAuth" - }, - { - "identity": "walletconnectswiftv2", - "name": "WalletConnectIdentity", - "type": "library", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectIdentity" - }, - { - "identity": "walletconnectswiftv2", - "name": "WalletConnectModal", - "type": "library", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectModal" - }, - { - "identity": "walletconnectswiftv2", - "name": "WalletConnectNetworking", - "type": "library", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectNetworking" - }, - { - "identity": "walletconnectswiftv2", - "name": "WalletConnectNotify", - "type": "library", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectNotify" - }, - { - "identity": "walletconnectswiftv2", - "name": "WalletConnectPairing", - "type": "library", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectPairing" - }, - { - "identity": "walletconnectswiftv2", - "name": "WalletConnectPush", - "type": "library", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectPush" - }, - { - "identity": "walletconnectswiftv2", - "name": "WalletConnectRouter", - "type": "library", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectRouter" - }, - { - "identity": "walletconnectswiftv2", - "name": "WalletConnectVerify", - "type": "library", - "label": "@swiftpkg_walletconnectswiftv2//:WalletConnectVerify" - }, - { - "identity": "walletconnectswiftv2", - "name": "Web3Wallet", - "type": "library", - "label": "@swiftpkg_walletconnectswiftv2//:Web3Wallet" - }, - { - "identity": "xcodeedit", - "name": "XcodeEdit", - "type": "library", - "label": "@swiftpkg_xcodeedit//:XcodeEdit" - } - ], - "packages": [ - { - "name": "swiftpkg_anycodable", - "identity": "anycodable", - "remote": { - "commit": "862808b2070cd908cb04f9aafe7de83d35f81b05", - "remote": "https://github.com/Flight-School/AnyCodable", - "version": "0.6.7" - } - }, - { - "name": "swiftpkg_bigdecimal", - "identity": "bigdecimal", - "remote": { - "commit": "04d17040e4615fbfda3a882b9881f6841f4bf557", - "remote": "https://github.com/Zollerboy1/BigDecimal.git", - "version": "1.0.2" - } - }, - { - "name": "swiftpkg_bigint", - "identity": "bigint", - "remote": { - "commit": "0ed110f7555c34ff468e72e1686e59721f2b0da6", - "remote": "https://github.com/attaswift/BigInt", - "version": "5.3.0" - } - }, - { - "name": "swiftpkg_core_swift", - "identity": "core-swift", - "remote": { - "commit": "f03e7c89c56aaafdb3b22a8ab5ebfefb3fb018a6", - "remote": "https://github.com/denis15yo/core-swift.git", - "branch": "release/1.0.0" - } - }, - { - "name": "swiftpkg_cryptoswift", - "identity": "cryptoswift", - "remote": { - "commit": "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", - "remote": "https://github.com/krzyzanowskim/CryptoSwift.git", - "version": "1.8.2" - } - }, - { - "name": "swiftpkg_curvelib.swift", - "identity": "curvelib.swift", - "remote": { - "commit": "9f88bd5e56d1df443a908f7a7e81ae4f4d9170ea", - "remote": "https://github.com/tkey/curvelib.swift", - "version": "1.0.1" - } - }, - { - "name": "swiftpkg_factory", - "identity": "factory", - "remote": { - "commit": "587995f7d5cc667951d635fbf6b4252324ba0439", - "remote": "https://github.com/hmlongco/Factory.git", - "version": "2.3.2" - } - }, - { - "name": "swiftpkg_fetch_node_details_swift", - "identity": "fetch-node-details-swift", - "remote": { - "commit": "4bd96c33ba8d02d9e27190c5c7cedf09cfdfd656", - "remote": "https://github.com/torusresearch/fetch-node-details-swift.git", - "version": "6.0.3" - } - }, - { - "name": "swiftpkg_floatingpanel", - "identity": "floatingpanel", - "remote": { - "commit": "29185a47bd9f062c060e097641b863ef07f60ba7", - "remote": "https://github.com/scenee/FloatingPanel", - "version": "2.8.4" - } - }, - { - "name": "swiftpkg_grdb.swift", - "identity": "grdb.swift", - "remote": { - "commit": "afc958017ee4feefd3c61c8e2cddf81d079d2e39", - "remote": "https://github.com/denis15yo/GRDB.swift.git", - "branch": "master" - } - }, - { - "name": "swiftpkg_keychain_swift", - "identity": "keychain-swift", - "remote": { - "commit": "d108a1fa6189e661f91560548ef48651ed8d93b9", - "remote": "https://github.com/evgenyneu/keychain-swift.git", - "version": "20.0.0" - } - }, - { - "name": "swiftpkg_lnextensionexecutor", - "identity": "lnextensionexecutor", - "remote": { - "commit": "c0226dcd7d653d4c22dd16ccd72619c86b610c2d", - "remote": "https://github.com/LeoNatan/LNExtensionExecutor", - "version": "1.3.0" - } - }, - { - "name": "swiftpkg_navigation_stack_backport", - "identity": "navigation-stack-backport", - "remote": { - "commit": "66716ce9c31198931c2275a0b69de2fdaa687e74", - "remote": "https://github.com/denis15yo/navigation-stack-backport.git", - "branch": "main" - } - }, - { - "name": "swiftpkg_nicegram_assistant_ios", - "identity": "nicegram-assistant-ios", - "remote": { - "commit": "033d4882165ee343a5db09c93cbca462f27f5732", - "remote": "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", - "branch": "next-wallet-release" - } - }, - { - "name": "swiftpkg_nicegram_wallet_ios", - "identity": "nicegram-wallet-ios", - "remote": { - "commit": "2d647862a3111bfc0f6c0f9e966fdc8ff1a21d34", - "remote": "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", - "branch": "next-wallet-release" - } - }, - { - "name": "swiftpkg_qrcode", - "identity": "qrcode", - "remote": { - "commit": "263f280d2c8144adfb0b6676109846cfc8dd552b", - "remote": "https://github.com/WalletConnect/QRCode", - "version": "14.3.1" - } - }, - { - "name": "swiftpkg_r.swift", - "identity": "r.swift", - "remote": { - "commit": "4a0f8c97f1baa27d165dc801982c55bbf51126e5", - "remote": "https://github.com/denis15yo/R.swift.git", - "branch": "main" - } - }, - { - "name": "swiftpkg_sdwebimage", - "identity": "sdwebimage", - "remote": { - "commit": "86e9185ef41c4238a93ad8efe61ddeb701e80bbf", - "remote": "https://github.com/SDWebImage/SDWebImage.git", - "version": "5.19.5" - } - }, - { - "name": "swiftpkg_session_manager_swift", - "identity": "session-manager-swift", - "remote": { - "commit": "20cc7bff065d7fe53164d17e7714a3f17d4cea2a", - "remote": "https://github.com/Web3Auth/session-manager-swift.git", - "version": "4.0.2" - } - }, - { - "name": "swiftpkg_single_factor_auth_swift", - "identity": "single-factor-auth-swift", - "remote": { - "commit": "4caaaa858950b25ea420dbba79de6b4c58801db4", - "remote": "https://github.com/Web3Auth/single-factor-auth-swift.git", - "version": "6.0.0" - } - }, - { - "name": "swiftpkg_snapkit", - "identity": "snapkit", - "remote": { - "commit": "2842e6e84e82eb9a8dac0100ca90d9444b0307f4", - "remote": "https://github.com/SnapKit/SnapKit.git", - "version": "5.7.1" - } - }, - { - "name": "swiftpkg_starscream", - "identity": "starscream", - "remote": { - "commit": "c6bfd1af48efcc9a9ad203665db12375ba6b145a", - "remote": "https://github.com/daltoniam/Starscream.git", - "version": "4.0.8" - } - }, - { - "name": "swiftpkg_subscriptionanalytics_ios", - "identity": "subscriptionanalytics-ios", - "remote": { - "commit": "53bfc6c6f26322ec647b87c338a071714ac69420", - "remote": "git@bitbucket.org:mobyrix/subscriptionanalytics-ios.git", - "version": "0.4.3" - } - }, - { - "name": "swiftpkg_swift_argument_parser", - "identity": "swift-argument-parser", - "remote": { - "commit": "41982a3656a71c768319979febd796c6fd111d5c", - "remote": "https://github.com/apple/swift-argument-parser", - "version": "1.5.0" - } - }, - { - "name": "swiftpkg_swift_collections", - "identity": "swift-collections", - "remote": { - "commit": "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", - "remote": "https://github.com/apple/swift-collections", - "version": "1.1.2" - } - }, - { - "name": "swiftpkg_swift_http_types", - "identity": "swift-http-types", - "remote": { - "commit": "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd", - "remote": "https://github.com/apple/swift-http-types", - "version": "1.3.0" - } - }, - { - "name": "swiftpkg_swift_numerics", - "identity": "swift-numerics", - "remote": { - "commit": "0a5bc04095a675662cf24757cc0640aa2204253b", - "remote": "https://github.com/apple/swift-numerics.git", - "version": "1.0.2" - } - }, - { - "name": "swiftpkg_swift_openapi_runtime", - "identity": "swift-openapi-runtime", - "remote": { - "commit": "a51b3bd6f2151e9a6f792ca6937a7242c4758768", - "remote": "https://github.com/apple/swift-openapi-runtime", - "version": "0.3.6" - } - }, - { - "name": "swiftpkg_swift_openapi_urlsession", - "identity": "swift-openapi-urlsession", - "remote": { - "commit": "9229842c63e9fc3bbd32c661d8274b4d9d8715f1", - "remote": "https://github.com/apple/swift-openapi-urlsession.git", - "version": "0.3.1" - } - }, - { - "name": "swiftpkg_swift_qrcode_generator", - "identity": "swift-qrcode-generator", - "remote": { - "commit": "5ca09b6a2ad190f94aa3d6ddef45b187f8c0343b", - "remote": "https://github.com/dagronf/swift-qrcode-generator", - "version": "1.0.3" - } - }, - { - "name": "swiftpkg_swiftimagereadwrite", - "identity": "swiftimagereadwrite", - "remote": { - "commit": "5596407d1cf61b953b8e658fa8636a471df3c509", - "remote": "https://github.com/dagronf/SwiftImageReadWrite", - "version": "1.1.6" - } - }, - { - "name": "swiftpkg_swiftystorekit", - "identity": "swiftystorekit", - "remote": { - "commit": "9ce911639680113dac9b554d6243e406a9758ebe", - "remote": "https://github.com/bizz84/SwiftyStoreKit.git", - "version": "0.16.4" - } - }, - { - "name": "swiftpkg_tkey_ios", - "identity": "tkey-ios", - "remote": { - "commit": "c107450f0675351a9a1eaaefe60bcfa285ff1f9e", - "remote": "https://github.com/tkey/tkey-ios.git", - "version": "0.2.1" - } - }, - { - "name": "swiftpkg_ton_api_swift", - "identity": "ton-api-swift", - "remote": { - "commit": "c1d5a7912480d1794097f4fb8241c3176f394384", - "remote": "https://github.com/tonkeeper/ton-api-swift", - "version": "0.1.6" - } - }, - { - "name": "swiftpkg_ton_swift", - "identity": "ton-swift", - "remote": { - "commit": "e4c3def222afc125f7ee83c1569004e31f0cd05c", - "remote": "https://github.com/denis15yo/ton-swift.git", - "branch": "main" - } - }, - { - "name": "swiftpkg_torus_utils_swift", - "identity": "torus-utils-swift", - "remote": { - "commit": "608c28404c506983bfec7bbd957632fc0544db8c", - "remote": "https://github.com/torusresearch/torus-utils-swift.git", - "version": "8.1.2" - } - }, - { - "name": "swiftpkg_tweetnacl_swiftwrap", - "identity": "tweetnacl-swiftwrap", - "remote": { - "commit": "f8fd111642bf2336b11ef9ea828510693106e954", - "remote": "https://github.com/bitmark-inc/tweetnacl-swiftwrap", - "version": "1.1.0" - } - }, - { - "name": "swiftpkg_wallet_core", - "identity": "wallet-core", - "remote": { - "commit": "f14ae4c31e652b293bd5d892b128afc0fa6102c5", - "remote": "https://github.com/trustwallet/wallet-core.git", - "version": "4.1.1" - } - }, - { - "name": "swiftpkg_walletconnectswiftv2", - "identity": "walletconnectswiftv2", - "remote": { - "commit": "1eacd732e321c9511859d7e73303d61d82af4d46", - "remote": "https://github.com/denis15yo/WalletConnectSwiftV2.git", - "branch": "develop" - } - }, - { - "name": "swiftpkg_xcodeedit", - "identity": "xcodeedit", - "remote": { - "commit": "b6b67389a0f1a6fdd9c6457a8ab5b02eaab13c5c", - "remote": "https://github.com/tomlokhorst/XcodeEdit", - "version": "2.9.2" - } - } - ] -} \ No newline at end of file diff --git a/versions.json b/versions.json index ffcac695785..391b8b9ce39 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "1.6.9", + "app": "1.7.0", "xcode": "15.2", "bazel": "7.1.1", "macos": "13.0"