From 2db08cde8924eda0fa3732897cbedb68e8d100e1 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 13 Jan 2026 19:21:49 +0400 Subject: [PATCH] Glass --- ...uthorizationSequenceSignUpController.swift | 2 +- .../Sources/BrowserAddressListComponent.swift | 2 +- .../Sources/BrowserBookmarksScreen.swift | 2 +- .../Sources/BrowserInstantPageContent.swift | 2 +- .../BrowserUI/Sources/BrowserScreen.swift | 4 +- .../Sources/CallListController.swift | 4 +- .../Sources/ChatListController.swift | 30 +- .../ChatListFilterPresetController.swift | 4 +- .../Sources/ChatListSearchContainerNode.swift | 6 +- .../Sources/ChatListSearchListPaneNode.swift | 2 +- .../ChatSendMessageContextScreen.swift | 6 +- submodules/ComponentFlow/BUILD | 3 +- .../Source/Base/Transition.swift | 17 +- .../Sources/ContactsController.swift | 10 +- .../Sources/ContactsControllerNode.swift | 4 +- .../ContextUI/Sources/ContextController.swift | 2911 +++-------------- .../ContextUI/Sources/PeekController.swift | 172 +- .../Sources/PeekControllerContent.swift | 38 - .../PeekControllerGestureRecognizer.swift | 19 +- .../ContextUI/Sources/PinchController.swift | 489 +-- .../Sources/PinchSourceContainerNode.swift | 226 ++ .../Source/ContextContentContainerNode.swift | 2 +- .../Source/ContextContentSourceNode.swift | 19 + submodules/Display/Source/NavigationBar.swift | 2 + .../DrawingUI/Sources/DrawingScreen.swift | 4 +- .../Sources/FeaturedStickersScreen.swift | 2 +- .../Sources/Items/ChatImageGalleryItem.swift | 2 +- .../Items/UniversalVideoGalleryItem.swift | 2 +- .../ImportStickerPackControllerNode.swift | 2 +- .../Sources/InstantPageControllerNode.swift | 2 +- .../FolderInviteLinkListController.swift | 2 +- .../Sources/InviteLinkInviteController.swift | 2 +- .../Sources/InviteLinkListController.swift | 4 +- .../Sources/InviteLinkViewController.swift | 2 +- .../Sources/InviteRequestsController.swift | 2 +- .../Sources/InviteRequestsSearchItem.swift | 2 +- .../Sources/ItemListInviteRequestItem.swift | 2 +- .../Sources/MediaPickerScreen.swift | 6 +- .../Sources/ChannelVisibilityController.swift | 2 +- .../Sources/PeersNearbyController.swift | 2 +- .../StorageUsageExceptionsScreen.swift | 2 +- .../Sources/ThemePickerController.swift | 4 +- .../Themes/ThemeSettingsController.swift | 4 +- .../Sources/ShareControllerNode.swift | 2 +- .../Sources/ChannelStatsController.swift | 2 +- .../Sources/MessageStatsController.swift | 2 +- .../Sources/StickerPackScreen.swift | 4 +- .../MediaNavigationAccessoryHeaderNode.swift | 2 +- .../Components/MediaStreamComponent.swift | 2 +- .../VideoChatParticipantsComponent.swift | 2 +- .../Sources/VideoChatScreenMoreMenu.swift | 2 +- ...ideoChatScreenParticipantContextMenu.swift | 4 +- .../Sources/VoiceChatMainStageNode.swift | 2 +- .../Sources/VoiceChatParticipantItem.swift | 2 +- .../Sources/VoiceChatPeerProfileNode.swift | 2 +- .../Sources/ApiUtils/TelegramUser.swift | 3 + .../SyncCore/SyncCore_TelegramUser.swift | 1 + submodules/TelegramUI/BUILD | 1 + .../AdsInfoScreen/Sources/AdsInfoScreen.swift | 2 +- .../CameraScreen/Sources/CameraCollage.swift | 2 +- .../ChatRecentActionsControllerNode.swift | 2 +- .../Chat/ChatSendAsContextMenu/BUILD | 1 + .../ChatSendAsPeerListContextItem.swift | 1 + .../Sources/ChatSendStarsScreen.swift | 2 +- .../Sources/ChatSideTopicsPanel.swift | 26 +- .../Sources/ChatTextInputPanelNode.swift | 2 +- .../Sources/ChatEntityKeyboardInputNode.swift | 4 +- .../Sources/ChatFolderLinkPreviewScreen.swift | 2 +- .../Sources/ChatListHeaderComponent.swift | 22 +- .../Sources/ChatTitleComponent.swift | 13 +- .../Components/ContextControllerImpl/BUILD | 41 + .../Sources/ContextActionNode.swift | 14 +- .../Sources/ContextActionsContainerNode.swift | 1 + .../ContextControllerActionsStackNode.swift | 145 +- ...tControllerExtractedPresentationNode.swift | 200 +- .../Sources/ContextControllerImpl.swift | 2203 +++++++++++++ .../ContextControllerPresentationNode.swift | 1 + .../Sources/ContextSourceContainer.swift | 9 +- .../Sources/PeekController.swift | 120 + .../Sources/PeekControllerNode.swift | 7 +- .../Sources/PinchController.swift | 262 ++ .../Sources/ReactionPreviewView.swift | 0 .../EdgeEffect/Sources/EdgeEffect.swift | 78 + .../Components/Gifts/GiftStoreScreen/BUILD | 1 + .../GiftAttributeListContextItem.swift | 1 + .../Sources/GiftStoreScreen.swift | 8 +- .../Sources/GiftAuctionBidScreen.swift | 2 +- .../Sources/GiftAuctionViewScreen.swift | 2 +- .../Sources/GiftViewScreen.swift | 2 +- .../Sources/GlassBackgroundComponent.swift | 145 +- .../Sources/LegacyMessageInputPanel.swift | 2 +- .../Components/MediaEditorScreen/BUILD | 1 + .../Sources/CreateLinkOptions.swift | 2 +- .../Sources/MediaEditorScreen.swift | 8 +- .../Sources/StickerPackListContextItem.swift | 1 + .../MediaNavigationAccessoryHeaderNode.swift | 2 +- .../StickersResultPanelComponent.swift | 2 +- .../Sources/NavigationBarImpl.swift | 18 +- .../Sources/AffiliateProgramSetupScreen.swift | 4 +- .../Sources/JoinAffiliateProgramScreen.swift | 2 +- .../Sources/PeerInfoChatListPaneNode.swift | 2 +- .../Sources/Panes/PeerInfoMembersPane.swift | 2 +- .../Sources/PeerInfoAvatarListNode.swift | 2 +- ...oHeaderNavigationButtonContainerNode.swift | 8 +- .../Sources/PeerInfoPaneContainerNode.swift | 2 +- .../Sources/PeerInfoScreen.swift | 29 +- .../PeerInfoScreenBusinessActions.swift | 4 +- .../Sources/PeerInfoScreenCallActions.swift | 4 +- ...eerInfoScreenDisplayGiftsContextMenu.swift | 9 +- ...ScreenDisplayMediaGalleryContextMenu.swift | 13 +- .../Sources/PeerInfoScreenOpenBio.swift | 2 +- .../Sources/PeerInfoScreenOpenBirthday.swift | 2 +- .../Sources/PeerInfoScreenOpenNote.swift | 2 +- .../Sources/PeerInfoScreenOpenPhone.swift | 2 +- .../Sources/PeerInfoScreenOpenUsername.swift | 2 +- .../PeerInfoScreenPerformButtonAction.swift | 4 +- .../Sources/PeerInfoSettingsTabActions.swift | 2 +- .../Sources/PeerInfoStoryGridScreen.swift | 2 +- .../Sources/AddGiftsScreen.swift | 2 +- .../Sources/PeerInfoGiftsPaneNode.swift | 4 +- .../Sources/PeerInfoStoryPaneNode.swift | 4 +- .../Sources/PeerSelectionControllerNode.swift | 2 +- .../Sources/BusinessLinksSetupScreen.swift | 2 +- .../Sources/ShareWithPeersScreen.swift | 2 +- .../Sources/StickerPickerScreen.swift | 2 +- .../Sources/StorageUsageScreen.swift | 8 +- .../StoryContentLiveChatComponent.swift | 2 +- .../StoryItemSetContainerComponent.swift | 8 +- ...StoryItemSetContainerViewSendMessage.swift | 4 +- .../StoryItemSetViewListComponent.swift | 2 +- .../Sources/ChatTranslationPanelNode.swift | 2 +- .../TelegramUI/Sources/AppDelegate.swift | 58 + .../Chat/ChatControllerLoadDisplayNode.swift | 8 +- ...hatControllerOpenBankCardContextMenu.swift | 2 +- ...ChatControllerOpenCommandContextMenu.swift | 2 +- ...ChatControllerOpenHashtagContextMenu.swift | 2 +- .../ChatControllerOpenLinkContextMenu.swift | 2 +- ...ChatControllerOpenMessageContextMenu.swift | 2 +- .../Sources/Chat/ChatControllerOpenPeer.swift | 2 +- .../ChatControllerOpenPhoneContextMenu.swift | 2 +- ...hatControllerOpenTimecodeContextMenu.swift | 2 +- .../ChatControllerOpenTodoContextMenu.swift | 2 +- ...hatControllerOpenUsernameContextMenu.swift | 2 +- ...atControllerOpenViewOnceMediaMessage.swift | 2 +- .../Chat/ChatMessageActionOptions.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 20 +- .../Sources/ChatControllerNode.swift | 9 +- .../ChatControllerOpenCalendarSearch.swift | 2 +- ...rollerOpenMessageReactionContextMenu.swift | 4 +- .../Sources/ChatHistoryEntriesForView.swift | 2 +- .../ChatInterfaceTitlePanelNodes.swift | 4 +- .../ChatManagingBotTitlePanelNode.swift | 2 +- .../ChatSearchResultsContollerNode.swift | 2 +- .../ChatSearchTitleAccessoryPanelNode.swift | 2 +- .../ContactMultiselectionController.swift | 2 +- .../Sources/ContactSelectionController.swift | 2 +- .../Sources/CreateGroupController.swift | 2 +- .../EmojisChatInputContextPanelNode.swift | 2 +- ...textResultsChatInputContextPanelNode.swift | 2 +- ...rizontalStickersChatContextPanelNode.swift | 2 +- .../Sources/InlineReactionSearchPanel.swift | 2 +- .../OverlayAudioPlayerControllerNode.swift | 2 +- .../Sources/OverlayPlayerControlsNode.swift | 2 +- .../UIViewController+Navigation.h | 11 + .../UIViewController+Navigation.m | 66 + .../Sources/WallpaperEdgeEffectNodeImpl.swift | 2 +- .../WebUI/Sources/WebAppController.swift | 2 +- 167 files changed, 4322 insertions(+), 3473 deletions(-) delete mode 100644 submodules/ContextUI/Sources/PeekControllerContent.swift create mode 100644 submodules/ContextUI/Sources/PinchSourceContainerNode.swift create mode 100644 submodules/TelegramUI/Components/ContextControllerImpl/BUILD rename submodules/{ContextUI => TelegramUI/Components/ContextControllerImpl}/Sources/ContextActionNode.swift (98%) rename submodules/{ContextUI => TelegramUI/Components/ContextControllerImpl}/Sources/ContextActionsContainerNode.swift (99%) rename submodules/{ContextUI => TelegramUI/Components/ContextControllerImpl}/Sources/ContextControllerActionsStackNode.swift (95%) rename submodules/{ContextUI => TelegramUI/Components/ContextControllerImpl}/Sources/ContextControllerExtractedPresentationNode.swift (92%) create mode 100644 submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerImpl.swift rename submodules/{ContextUI => TelegramUI/Components/ContextControllerImpl}/Sources/ContextControllerPresentationNode.swift (98%) rename submodules/{ContextUI => TelegramUI/Components/ContextControllerImpl}/Sources/ContextSourceContainer.swift (99%) create mode 100644 submodules/TelegramUI/Components/ContextControllerImpl/Sources/PeekController.swift rename submodules/{ContextUI => TelegramUI/Components/ContextControllerImpl}/Sources/PeekControllerNode.swift (99%) create mode 100644 submodules/TelegramUI/Components/ContextControllerImpl/Sources/PinchController.swift rename submodules/{ContextUI => TelegramUI/Components/ContextControllerImpl}/Sources/ReactionPreviewView.swift (100%) diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift index 2ec0d70541..74d988bf45 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift @@ -138,7 +138,7 @@ final class AuthorizationSequenceSignUpController: ViewController { }))) - let contextController = ContextController(presentationData: self.presentationData, source: .reference(AuthorizationContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: .reference(AuthorizationContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.present(contextController, in: .window(.root)) } diff --git a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift index cd308ad441..23b1c03107 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift @@ -443,7 +443,7 @@ final class BrowserAddressListComponent: Component { } let items = ContextController.Items(content: .list(itemList)) - let controller = ContextController( + let controller = makeContextController( presentationData: presentationData, source: .extracted(BrowserAddressListContextExtractedContentSource(contentView: sourceView)), items: .single(items), diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index d5fa9632b5..9d9003b087 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -273,7 +273,7 @@ public final class BrowserBookmarksScreen: ViewController { }))) let items = ContextController.Items(content: .list(itemList)) - let controller = ContextController( + let controller = makeContextController( presentationData: presentationData, source: .extracted(BrowserBookmarksContextExtractedContentSource(contentNode: sourceNode)), items: .single(items), diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index cb03ff6edf..38beedd814 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -1198,7 +1198,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } private func activatePinchPreview(sourceNode: PinchSourceContainerNode) { - let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { [weak self] in + let pinchController = makePinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { [weak self] in guard let self else { return CGRect() } diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 9eac12ee87..022acc615f 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -1230,7 +1230,7 @@ public class BrowserScreen: ViewController, MinimizableController { return ContextController.Items(content: .list(items)) } - let contextController = ContextController(presentationData: self.presentationData, source: source, items: items) + let contextController = makeContextController(presentationData: self.presentationData, source: source, items: items) contextController.dismissed = { [weak content] in if let webContent = content as? BrowserWebContent { webContent.releaseInstantView() @@ -1342,7 +1342,7 @@ public class BrowserScreen: ViewController, MinimizableController { return } - let contextController = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items)))) + let contextController = makeContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items)))) self.controller?.present(contextController, in: .window(.root)) } diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 17435c6665..ab9ba5e3b8 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -517,7 +517,7 @@ public final class CallListController: TelegramBaseController { } } - let contextController = ContextController(presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: self, sourceNode: buttonNode.contentNode, keepInPlace: false, blurBackground: false)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + let contextController = makeContextController(presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: self, sourceNode: buttonNode.contentNode, keepInPlace: false, blurBackground: false)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) self.presentInGlobalOverlay(contextController) } @@ -782,7 +782,7 @@ public final class CallListController: TelegramBaseController { }) }))) - let controller = ContextController(presentationData: self.presentationData, source: .reference(CallListTabBarContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + let controller = makeContextController(presentationData: self.presentationData, source: .reference(CallListTabBarContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index c020faa7c1..818590448d 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1285,10 +1285,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if let sourceNode { - let controller = ContextController(presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, sourceView: sourceView, keepInPlace: keepInPlace)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + let controller = makeContextController(presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, sourceView: sourceView, keepInPlace: keepInPlace)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) } else if let sourceView { - let controller = ContextController(presentationData: self.presentationData, source: .reference(ChatListHeaderBarContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + let controller = makeContextController(presentationData: self.presentationData, source: .reference(ChatListHeaderBarContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) } }) @@ -1873,7 +1873,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController case let .groupReference(groupReference): let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .chatList(groupId: groupReference.groupId), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) chatListController.navigationPresentation = .master - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: archiveContextMenuItems(context: strongSelf.context, groupId: groupReference.groupId._asGroup(), chatListController: strongSelf) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: archiveContextMenuItems(context: strongSelf.context, groupId: groupReference.groupId._asGroup(), chatListController: strongSelf) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) case let .peer(peerData): let peer = peerData.peer @@ -1891,12 +1891,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) - let contextController = ContextController(presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: nil, isClosed: nil, chatListController: strongSelf, joined: joined, canSelect: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: nil, isClosed: nil, chatListController: strongSelf, joined: joined, canSelect: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } else { let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .forum(peerId: channel.id), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) chatListController.navigationPresentation = .master - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } else if let peer = peer.peer, peer.id == strongSelf.context.account.peerId, peerData.displayAsTopicList { @@ -1908,7 +1908,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController source = .controller(ContextControllerContentSourceImpl(controller: peerInfoController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } - let contextController = ContextController(presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } else { @@ -1926,7 +1926,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } - let contextController = ContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) dismissPreviewingImpl = { [weak self, weak contextController] animateIn in @@ -1960,7 +1960,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) - let contextController = ContextController(presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: isPinned, isClosed: threadInfo?.isClosed, chatListController: strongSelf, joined: joined, canSelect: true) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: isPinned, isClosed: threadInfo?.isClosed, chatListController: strongSelf, joined: joined, canSelect: true) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } @@ -1995,7 +1995,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if case let .channel(channel) = peer, channel.isForumOrMonoForum { let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .forum(peerId: channel.id), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) chatListController.navigationPresentation = .master - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } else { let contextContentSource: ContextContentSource @@ -2011,7 +2011,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController contextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } - let contextController = ContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: contextContentSource, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: contextContentSource, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } @@ -3425,7 +3425,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }))) } - let controller = ContextController(presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, sourceView: nil, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + let controller = makeContextController(presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, sourceView: nil, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) }) } @@ -3878,7 +3878,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: sourceController, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: sourceController, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) sourceController.presentInGlobalOverlay(contextController) }) } @@ -3938,7 +3938,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }))) } - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.presentInGlobalOverlay(contextController) }) } @@ -6206,7 +6206,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - let controller = ContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: .reference(ChatListTabBarContextReferenceContentSource(controller: strongSelf, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + let controller = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: .reference(ChatListTabBarContextReferenceContentSource(controller: strongSelf, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) }) } @@ -6408,7 +6408,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) }))) - let contextController = ContextController(presentationData: presentationData, source: .reference(AdsInfoContextReferenceContentSource(controller: controller, sourceView: referenceView, insets: .zero, contentInsets: .zero)), items: .single(ContextController.Items(content: .list(actions))), gesture: nil) + let contextController = makeContextController(presentationData: presentationData, source: .reference(AdsInfoContextReferenceContentSource(controller: controller, sourceView: referenceView, insets: .zero, contentInsets: .zero)), items: .single(ContextController.Items(content: .list(actions))), gesture: nil) controller.presentInGlobalOverlay(contextController) } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index da255877f8..616d68ef95 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -1927,7 +1927,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi }) }))) - let contextController = ContextController(presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }) }, @@ -1968,7 +1968,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi }) }))) - let contextController = ContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, updateTagColor: { color in diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 11af4ee929..2495e589ee 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -1174,7 +1174,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo return items } - let controller = ContextController(presentationData: self.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node, shouldBeDismissed: shouldBeDismissed)), items: items |> map { ContextController.Items(content: .list($0)) }, recognizer: nil, gesture: gesture) + let controller = makeContextController(presentationData: self.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node, shouldBeDismissed: shouldBeDismissed)), items: items |> map { ContextController.Items(content: .list($0)) }, recognizer: nil, gesture: gesture) self.presentInGlobalOverlay?(controller, nil) return @@ -1248,7 +1248,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo return items } - let controller = ContextController(presentationData: self.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: items |> map { ContextController.Items(content: .list($0)) }, recognizer: nil, gesture: gesture) + let controller = makeContextController(presentationData: self.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: items |> map { ContextController.Items(content: .list($0)) }, recognizer: nil, gesture: gesture) self.presentInGlobalOverlay?(controller, nil) } @@ -1312,7 +1312,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo switch previewData { case let .gallery(gallery): gallery.setHintWillBePresentedInPreviewingContext(true) - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay?(contextController, nil) case .instantPage: break diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 88a8cdf03d..9f74cd0ce5 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -5622,7 +5622,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { f(.default) self.searchScopePromise.set(.channels) }))) - let contextController = ContextController(presentationData: self.presentationData, source: .reference(ChatListSearchReferenceContentSource(sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + let contextController = makeContextController(presentationData: self.presentationData, source: .reference(ChatListSearchReferenceContentSource(sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) self.interaction.present(contextController, nil) }) } diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift index 8d38956739..5b91c21e3e 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift @@ -650,7 +650,7 @@ final class ChatSendMessageContextScreenComponent: Component { if let current = self.actionsStackNode { actionsStackNode = current - actionsStackNode.replace(item: ContextControllerActionsListStackItem( + actionsStackNode.replace(item: makeContextControllerActionsListStackItem( id: AnyHashable("items"), items: items, reactionItems: nil, @@ -660,7 +660,7 @@ final class ChatSendMessageContextScreenComponent: Component { dismissed: nil ), animated: !transition.animation.isImmediate) } else { - actionsStackNode = ContextControllerActionsStackNode( + actionsStackNode = makeContextControllerActionsStackNode( context: component.context, getController: { return nil @@ -681,7 +681,7 @@ final class ChatSendMessageContextScreenComponent: Component { } actionsStackNode.push( - item: ContextControllerActionsListStackItem( + item: makeContextControllerActionsListStackItem( id: AnyHashable("items"), items: items, reactionItems: nil, diff --git a/submodules/ComponentFlow/BUILD b/submodules/ComponentFlow/BUILD index 92f3ebdd2a..c6fb4eebd7 100644 --- a/submodules/ComponentFlow/BUILD +++ b/submodules/ComponentFlow/BUILD @@ -10,7 +10,8 @@ swift_library( "-warnings-as-errors", ], deps = [ - "//submodules/Display" + "//submodules/Display", + "//submodules/UIKitRuntimeUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 4beba45cc4..733f7d2356 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import Display +import UIKitRuntimeUtils #if targetEnvironment(simulator) @_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float @@ -84,7 +85,7 @@ public extension ComponentTransition.Animation { } public extension ComponentTransition { - func animateView(allowUserInteraction: Bool = false, delay: Double = 0.0, _ f: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) { + func animateView(allowUserInteraction: Bool = true, delay: Double = 0.0, _ f: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: f() @@ -94,9 +95,17 @@ public extension ComponentTransition { if allowUserInteraction { options.insert(.allowUserInteraction) } - UIView.animate(withDuration: duration, delay: delay, options: options, animations: { - f() - }, completion: completion) + if case .spring = curve { + CALayer.push(CALayerSpringParametersOverride()) + UIView.animate(withDuration: duration, delay: delay, usingSpringWithDamping: 500.0, initialSpringVelocity: 0.0, options: options, animations: { + f() + }, completion: completion) + CALayer.popSpringParametersOverride() + } else { + UIView.animate(withDuration: duration, delay: delay, options: options, animations: { + f() + }, completion: completion) + } } } } diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 293c10dbdf..af363a56a2 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -636,7 +636,13 @@ public class ContactsController: ViewController { }))) return items } - let contextController = ContextController(presentationData: self.presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + + var sourceView = sourceView + if let navigationBarComponentView = self.contactsNode.navigationBarView.view as? ChatListNavigationBar.View, let headerContentView = navigationBarComponentView.headerContent.view as? ChatListHeaderComponent.View, let value = headerContentView.navigationButtonContextContainer(sourceView: sourceView) { + sourceView = value + } + + let contextController = makeContextController(presentationData: self.presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) self.presentInGlobalOverlay(contextController) } @@ -800,7 +806,7 @@ public class ContactsController: ViewController { }) }))) - let controller = ContextController(presentationData: self.presentationData, source: .reference(ContactsTabBarContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + let controller = makeContextController(presentationData: self.presentationData, source: .reference(ContactsTabBarContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) } diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index cb9b316ce4..680428ae45 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -467,12 +467,12 @@ final class ContactsControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { let items = contactContextMenuItems(context: self.context, peerId: peer.id, contactsController: contactsController, isStories: isStories) |> map { ContextController.Items(content: .list($0)) } if isStories, let node = node?.subnodes?.first(where: { $0 is ContextExtractedContentContainingNode }) as? ContextExtractedContentContainingNode { - let controller = ContextController(presentationData: self.presentationData, source: .extracted(ContactContextExtractedContentSource(sourceNode: node, shouldBeDismissed: .single(false))), items: items, recognizer: nil, gesture: gesture) + let controller = makeContextController(presentationData: self.presentationData, source: .extracted(ContactContextExtractedContentSource(sourceNode: node, shouldBeDismissed: .single(false))), items: items, recognizer: nil, gesture: gesture) contactsController.presentInGlobalOverlay(controller) } else { let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) - let contextController = ContextController(presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: items, gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: items, gesture: gesture) contactsController.presentInGlobalOverlay(contextController) } } diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index dd6506d277..a82ee38cdb 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -15,8 +15,6 @@ import MultiAnimationRenderer import UndoUI import UIKitRuntimeUtils -private let animationDurationFactor: Double = 1.0 - public protocol ContextControllerProtocol: ViewController { var useComplexItemsTransitionAnimation: Bool { get set } var immediateItemsTransitionAnimation: Bool { get set } @@ -106,7 +104,7 @@ public final class ContextMenuActionItem { public let dismissWithResult: (ContextMenuActionResult) -> Void public let updateAction: (AnyHashable, ContextMenuActionItem) -> Void - init(controller: ContextControllerProtocol?, dismissWithResult: @escaping (ContextMenuActionResult) -> Void, updateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void) { + public init(controller: ContextControllerProtocol?, dismissWithResult: @escaping (ContextMenuActionResult) -> Void, updateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void) { self.controller = controller self.dismissWithResult = dismissWithResult self.updateAction = updateAction @@ -270,1838 +268,6 @@ public enum ContextMenuItem { case separator } -func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect { - let sourceWindowFrame = fromView.convert(frame, to: nil) - var targetWindowFrame = toView.convert(sourceWindowFrame, from: nil) - - if let fromWindow = fromView.window, let toWindow = toView.window { - targetWindowFrame.origin.x += toWindow.bounds.width - fromWindow.bounds.width - } - return targetWindowFrame -} - -final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelegate { - private weak var controller: ContextController? - private let context: AccountContext? - private var presentationData: PresentationData - - private let configuration: ContextController.Configuration - - private let legacySource: ContextContentSource - private var legacyItems: Signal - - let beginDismiss: (ContextMenuActionResult) -> Void - private let beganAnimatingOut: () -> Void - private let attemptTransitionControllerIntoNavigation: () -> Void - var dismissedForCancel: (() -> Void)? - private let getController: () -> ContextControllerProtocol? - private weak var gesture: ContextGesture? - - private var didSetItemsReady = false - let itemsReady = Promise() - let contentReady = Promise() - - private var currentItems: ContextController.Items? - private var currentActionsMinHeight: ContextController.ActionsHeight? - - private var validLayout: ContainerViewLayout? - - private let effectView: UIVisualEffectView - private var propertyAnimator: AnyObject? - private var displayLinkAnimator: DisplayLinkAnimator? - private let dimNode: ASDisplayNode - private let withoutBlurDimNode: ASDisplayNode - private let dismissNode: ASDisplayNode - private let dismissAccessibilityArea: AccessibilityAreaNode - - private var sourceContainer: ContextSourceContainer? - - private let clippingNode: ASDisplayNode - private let scrollNode: ASScrollNode - - private var originalProjectedContentViewFrame: (CGRect, CGRect)? - private var contentAreaInScreenSpace: CGRect? - private var customPosition: CGPoint? - private let contentContainerNode: ContextContentContainerNode - private var actionsContainerNode: ContextActionsContainerNode - - private var didCompleteAnimationIn = false - private var initialContinueGesturePoint: CGPoint? - private var didMoveFromInitialGesturePoint = false - private var highlightedActionNode: ContextActionNodeProtocol? - private var highlightedReaction: ReactionItem.Reaction? - - private let hapticFeedback = HapticFeedback() - - private var animatedIn = false - private var isAnimatingOut = false - - private let itemsDisposable = MetaDisposable() - - private let blurBackground: Bool - - var overlayWantsToBeBelowKeyboard: Bool { - guard let sourceContainer = self.sourceContainer else { - return false - } - return sourceContainer.overlayWantsToBeBelowKeyboard - } - - init( - controller: ContextController, - context: AccountContext?, - presentationData: PresentationData, - configuration: ContextController.Configuration, - beginDismiss: @escaping (ContextMenuActionResult) -> Void, - recognizer: TapLongTapOrDoubleTapGestureRecognizer?, - gesture: ContextGesture?, - beganAnimatingOut: @escaping () -> Void, - attemptTransitionControllerIntoNavigation: @escaping () -> Void - ) { - self.controller = controller - self.context = context - self.presentationData = presentationData - self.configuration = configuration - self.beginDismiss = beginDismiss - self.beganAnimatingOut = beganAnimatingOut - self.attemptTransitionControllerIntoNavigation = attemptTransitionControllerIntoNavigation - self.gesture = gesture - - self.legacySource = configuration.sources[0].source - self.legacyItems = configuration.sources[0].items - - self.getController = { [weak controller] in - return controller - } - - self.effectView = UIVisualEffectView() - if #available(iOS 9.0, *) { - } else { - if presentationData.theme.rootController.keyboardColor == .dark { - self.effectView.effect = UIBlurEffect(style: .dark) - } else { - self.effectView.effect = UIBlurEffect(style: .light) - } - self.effectView.alpha = 0.0 - } - - self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = presentationData.theme.contextMenu.dimColor - self.dimNode.alpha = 0.0 - - self.withoutBlurDimNode = ASDisplayNode() - self.withoutBlurDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.4) - self.withoutBlurDimNode.alpha = 0.0 - - self.dismissNode = ASDisplayNode() - self.dismissAccessibilityArea = AccessibilityAreaNode() - self.dismissAccessibilityArea.accessibilityLabel = presentationData.strings.VoiceOver_DismissContextMenu - self.dismissAccessibilityArea.accessibilityTraits = .button - - self.clippingNode = ASDisplayNode() - self.clippingNode.clipsToBounds = true - - self.scrollNode = ASScrollNode() - self.scrollNode.canCancelAllTouchesInViews = true - self.scrollNode.view.delaysContentTouches = false - self.scrollNode.view.showsVerticalScrollIndicator = false - if #available(iOS 11.0, *) { - self.scrollNode.view.contentInsetAdjustmentBehavior = .never - } - - self.contentContainerNode = ContextContentContainerNode() - - var feedbackTap: (() -> Void)? - var updateLayout: (() -> Void)? - - var blurBackground = true - if let mainSource = configuration.sources.first(where: { $0.id == configuration.initialId }) { - if case .reference = mainSource.source { - blurBackground = false - } else if case let .extracted(extractedSource) = mainSource.source, !extractedSource.blurBackground { - blurBackground = false - } - } - self.blurBackground = blurBackground - - self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: ContextController.Items(), getController: { [weak controller] in - return controller - }, actionSelected: { result in - beginDismiss(result) - }, requestLayout: { - updateLayout?() - }, feedbackTap: { - feedbackTap?() - }, blurBackground: blurBackground) - - super.init() - - feedbackTap = { [weak self] in - self?.hapticFeedback.tap() - } - - updateLayout = { [weak self] in - self?.updateLayout() - } - - self.scrollNode.view.delegate = self.wrappedScrollViewDelegate - - if blurBackground { - self.view.addSubview(self.effectView) - self.addSubnode(self.dimNode) - self.addSubnode(self.withoutBlurDimNode) - } - - self.addSubnode(self.clippingNode) - - self.clippingNode.addSubnode(self.scrollNode) - self.scrollNode.addSubnode(self.dismissNode) - self.scrollNode.addSubnode(self.dismissAccessibilityArea) - - self.scrollNode.addSubnode(self.actionsContainerNode) - - if let recognizer = recognizer { - recognizer.externalUpdated = { [weak self, weak recognizer] view, point in - guard let strongSelf = self, let _ = recognizer else { - return - } - let localPoint = strongSelf.view.convert(point, from: view) - let initialPoint: CGPoint - if let current = strongSelf.initialContinueGesturePoint { - initialPoint = current - } else { - initialPoint = localPoint - strongSelf.initialContinueGesturePoint = localPoint - } - if strongSelf.didCompleteAnimationIn { - if !strongSelf.didMoveFromInitialGesturePoint { - let distance = abs(localPoint.y - initialPoint.y) - if distance > 12.0 { - strongSelf.didMoveFromInitialGesturePoint = true - } - } - if strongSelf.didMoveFromInitialGesturePoint { - if let sourceContainer = strongSelf.sourceContainer { - let presentationPoint = strongSelf.view.convert(localPoint, to: sourceContainer.view) - sourceContainer.highlightGestureMoved(location: presentationPoint, hover: false) - } else { - let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) - let actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) - if strongSelf.highlightedActionNode !== actionNode { - strongSelf.highlightedActionNode?.setIsHighlighted(false) - strongSelf.highlightedActionNode = actionNode - if let actionNode = actionNode { - actionNode.setIsHighlighted(true) - strongSelf.hapticFeedback.tap() - } - } - } - } - } - } - recognizer.externalEnded = { [weak self, weak recognizer] viewAndPoint in - guard let strongSelf = self, let recognizer = recognizer else { - return - } - recognizer.externalUpdated = nil - if strongSelf.didMoveFromInitialGesturePoint { - if let sourceContainer = strongSelf.sourceContainer { - sourceContainer.highlightGestureFinished(performAction: viewAndPoint != nil) - } else { - if let (_, _) = viewAndPoint { - if let highlightedActionNode = strongSelf.highlightedActionNode { - strongSelf.highlightedActionNode = nil - highlightedActionNode.performAction() - } - } else { - if let highlightedActionNode = strongSelf.highlightedActionNode { - strongSelf.highlightedActionNode = nil - highlightedActionNode.setIsHighlighted(false) - } - } - } - } - } - } else if let gesture = gesture { - gesture.externalUpdated = { [weak self, weak gesture] view, point in - guard let strongSelf = self, let _ = gesture else { - return - } - let localPoint: CGPoint - if let layout = strongSelf.validLayout, layout.metrics.isTablet, layout.size.width > layout.size.height, let view { - localPoint = view.convert(point, to: nil) - } else { - localPoint = strongSelf.view.convert(point, from: view) - } - let initialPoint: CGPoint - if let current = strongSelf.initialContinueGesturePoint { - initialPoint = current - } else { - initialPoint = localPoint - strongSelf.initialContinueGesturePoint = localPoint - } - if strongSelf.didCompleteAnimationIn { - if !strongSelf.didMoveFromInitialGesturePoint { - let distance = abs(localPoint.y - initialPoint.y) - if distance > 4.0 { - strongSelf.didMoveFromInitialGesturePoint = true - } - } - if strongSelf.didMoveFromInitialGesturePoint { - if let sourceContainer = strongSelf.sourceContainer { - let presentationPoint = strongSelf.view.convert(localPoint, to: sourceContainer.view) - sourceContainer.highlightGestureMoved(location: presentationPoint, hover: false) - } else { - let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) - var actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) - if let actionNodeValue = actionNode, !actionNodeValue.isActionEnabled { - actionNode = nil - } - - if strongSelf.highlightedActionNode !== actionNode { - strongSelf.highlightedActionNode?.setIsHighlighted(false) - strongSelf.highlightedActionNode = actionNode - if let actionNode = actionNode { - actionNode.setIsHighlighted(true) - strongSelf.hapticFeedback.tap() - } - } - } - } - } - } - gesture.externalEnded = { [weak self, weak gesture] viewAndPoint in - guard let strongSelf = self, let gesture = gesture else { - return - } - gesture.externalUpdated = nil - if strongSelf.didMoveFromInitialGesturePoint { - if let sourceContainer = strongSelf.sourceContainer { - sourceContainer.highlightGestureFinished(performAction: viewAndPoint != nil) - } else { - if let (_, _) = viewAndPoint { - if let highlightedActionNode = strongSelf.highlightedActionNode { - strongSelf.highlightedActionNode = nil - highlightedActionNode.performAction() - } - } else { - if let highlightedActionNode = strongSelf.highlightedActionNode { - strongSelf.highlightedActionNode = nil - highlightedActionNode.setIsHighlighted(false) - } - } - } - } - } - } - - self.initializeContent() - - self.dismissAccessibilityArea.activate = { [weak self] in - self?.dimNodeTapped() - return true - } - - if controller.disableScreenshots { - setLayerDisableScreenshots(self.layer, true) - } - } - - deinit { - if let propertyAnimator = self.propertyAnimator { - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator - propertyAnimator?.stopAnimation(true) - } - } - - self.itemsDisposable.dispose() - } - - override func didLoad() { - super.didLoad() - - self.dismissNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTapped))) - - if #available(iOS 13.0, *) { - self.view.addGestureRecognizer(UIHoverGestureRecognizer(target: self, action: #selector(self.hoverGesture(_:)))) - } - } - - @objc private func dimNodeTapped() { - guard self.animatedIn else { - return - } - self.dismissedForCancel?() - self.beginDismiss(.default) - } - - @available(iOS 13.0, *) - @objc private func hoverGesture(_ gestureRecognizer: UIHoverGestureRecognizer) { - guard self.didCompleteAnimationIn else { - return - } - - let localPoint = gestureRecognizer.location(in: self.view) - - switch gestureRecognizer.state { - case .changed: - if let sourceContainer = self.sourceContainer { - let presentationPoint = self.view.convert(localPoint, to: sourceContainer.view) - sourceContainer.highlightGestureMoved(location: presentationPoint, hover: true) - } else { - let actionPoint = self.view.convert(localPoint, to: self.actionsContainerNode.view) - let actionNode = self.actionsContainerNode.actionNode(at: actionPoint) - if self.highlightedActionNode !== actionNode { - self.highlightedActionNode?.setIsHighlighted(false) - self.highlightedActionNode = actionNode - if let actionNode = actionNode { - actionNode.setIsHighlighted(true) - } - } - } - case .ended, .cancelled: - if let sourceContainer = self.sourceContainer { - sourceContainer.highlightGestureMoved(location: CGPoint(x: -1, y: -1), hover: true) - } else { - if let highlightedActionNode = self.highlightedActionNode { - self.highlightedActionNode = nil - highlightedActionNode.setIsHighlighted(false) - } - } - default: - break - } - } - - private func initializeContent() { - if self.configuration.sources.count == 1 { - switch self.configuration.sources[0].source { - case .location: - break - case let .reference(source): - if let controller = self.getController() as? ContextController, controller.workaroundUseLegacyImplementation { - self.contentReady.set(.single(true)) - - let transitionInfo = source.transitionInfo() - if let transitionInfo { - let referenceView = transitionInfo.referenceView - self.contentContainerNode.contentNode = .reference(view: referenceView) - self.contentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace - self.customPosition = transitionInfo.customPosition - - var projectedFrame = convertFrame(referenceView.bounds, from: referenceView, to: self.view) - projectedFrame.origin.x += transitionInfo.insets.left - projectedFrame.size.width -= transitionInfo.insets.left + transitionInfo.insets.right - projectedFrame.origin.y += transitionInfo.insets.top - projectedFrame.size.width -= transitionInfo.insets.top + transitionInfo.insets.bottom - self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) - } - - self.itemsDisposable.set((self.configuration.sources[0].items - |> deliverOnMainQueue).start(next: { [weak self] items in - self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale) - })) - - return - } - case .extracted: - break - case let .controller(source): - if let controller = self.getController() as? ContextController, controller.workaroundUseLegacyImplementation { - self.contentReady.set(source.controller.ready.get()) - - let transitionInfo = source.transitionInfo() - if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() { - let contentParentNode = ContextControllerContentNode(sourceView: sourceView, controller: source.controller, tapped: { [weak self] in - self?.attemptTransitionControllerIntoNavigation() - }) - self.contentContainerNode.contentNode = .controller(contentParentNode) - self.scrollNode.addSubnode(self.contentContainerNode) - self.contentContainerNode.clipsToBounds = true - self.contentContainerNode.cornerRadius = 14.0 - self.contentContainerNode.addSubnode(contentParentNode) - - let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view) - self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) - } - - self.itemsDisposable.set((self.configuration.sources[0].items - |> deliverOnMainQueue).start(next: { [weak self] items in - self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale) - })) - - return - } - } - } - - if let controller = self.controller { - let sourceContainer = ContextSourceContainer(controller: controller, configuration: self.configuration, context: self.context) - self.contentReady.set(sourceContainer.ready.get()) - self.itemsReady.set(.single(true)) - self.sourceContainer = sourceContainer - self.addSubnode(sourceContainer) - } - } - - func animateIn() { - self.gesture?.endPressedAppearance() - self.hapticFeedback.impact() - - if let sourceContainer = self.sourceContainer { - self.didCompleteAnimationIn = true - sourceContainer.animateIn() - return - } - - switch self.legacySource { - case .location, .reference: - break - case .extracted: - if let contentAreaInScreenSpace = self.contentAreaInScreenSpace, let maybeContentNode = self.contentContainerNode.contentNode, case .extracted = maybeContentNode { - var updatedContentAreaInScreenSpace = contentAreaInScreenSpace - updatedContentAreaInScreenSpace.origin.x = 0.0 - updatedContentAreaInScreenSpace.size.width = self.bounds.width - - self.clippingNode.layer.animateFrame(from: updatedContentAreaInScreenSpace, to: self.clippingNode.frame, duration: 0.18 * animationDurationFactor, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) - self.clippingNode.layer.animateBoundsOriginYAdditive(from: updatedContentAreaInScreenSpace.minY, to: 0.0, duration: 0.18 * animationDurationFactor, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) - } - case let .controller(source): - let transitionInfo = source.transitionInfo() - if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() { - let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view) - self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) - - var updatedContentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace - updatedContentAreaInScreenSpace.origin.x = 0.0 - updatedContentAreaInScreenSpace.size.width = self.bounds.width - self.contentAreaInScreenSpace = updatedContentAreaInScreenSpace - } - } - - if let validLayout = self.validLayout { - self.updateLayout(layout: validLayout, transition: .immediate, previousActionsContainerNode: nil) - } - - if !self.dimNode.isHidden { - self.dimNode.alpha = 1.0 - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) - } else { - self.withoutBlurDimNode.alpha = 1.0 - self.withoutBlurDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) - } - - if #available(iOS 10.0, *) { - if let propertyAnimator = self.propertyAnimator { - let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator - propertyAnimator?.stopAnimation(true) - } - self.effectView.effect = makeCustomZoomBlurEffect(isLight: presentationData.theme.rootController.keyboardColor == .light) - self.effectView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) - self.propertyAnimator = UIViewPropertyAnimator(duration: 0.2 * animationDurationFactor * UIView.animationDurationFactor(), curve: .easeInOut, animations: { - }) - } - - if let _ = self.propertyAnimator { - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] value in - (self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value - }, completion: { [weak self] in - self?.didCompleteAnimationIn = true - self?.hapticFeedback.prepareTap() - self?.actionsContainerNode.animateIn() - }) - } - } else { - UIView.animate(withDuration: 0.2 * animationDurationFactor, animations: { - self.effectView.effect = makeCustomZoomBlurEffect(isLight: self.presentationData.theme.rootController.keyboardColor == .light) - }, completion: { [weak self] _ in - self?.didCompleteAnimationIn = true - self?.actionsContainerNode.animateIn() - }) - } - - if let contentNode = self.contentContainerNode.contentNode { - switch contentNode { - case .reference: - let springDuration: Double = 0.42 * animationDurationFactor - let springDamping: CGFloat = 104.0 - - self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) - self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) - - if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { - let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) - - let localContentSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.contentContainerNode.view.superview) - - self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) - let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y) - self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { [weak self] _ in - self?.animatedIn = true - }) - } - case let .extracted(extracted, keepInPlace): - let springDuration: Double = 0.42 * animationDurationFactor - var springDamping: CGFloat = 104.0 - if case let .extracted(source) = self.legacySource, source.centerVertically { - springDamping = 124.0 - } - - self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) - self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) - - if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { - let contentParentNode = extracted - let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) - - var actionsDuration = springDuration - var actionsOffset: CGFloat = 0.0 - var contentDuration = springDuration - if case let .extracted(source) = self.legacySource, source.centerVertically { - actionsOffset = -(originalProjectedContentViewFrame.1.height - originalProjectedContentViewFrame.0.height) * 0.57 - actionsDuration *= 1.0 - contentDuration *= 0.9 - } - - let localContentSourceFrame: CGRect - if keepInPlace { - localContentSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.contentContainerNode.view.superview) - } else { - localContentSourceFrame = localSourceFrame - } - - self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y + actionsOffset)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: actionsDuration, initialVelocity: 0.0, damping: springDamping, additive: true) - let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY) - self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: contentDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { [weak self] _ in - self?.clippingNode.view.mask = nil - self?.animatedIn = true - }) - contentParentNode.applyAbsoluteOffsetSpring?(-contentContainerOffset.y, springDuration, springDamping) - } - - extracted.willUpdateIsExtractedToContextPreview?(true, .animated(duration: 0.2, curve: .easeInOut)) - case .controller: - let springDuration: Double = 0.52 * animationDurationFactor - let springDamping: CGFloat = 110.0 - - self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) - self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) - self.contentContainerNode.allowsGroupOpacity = true - self.contentContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor, completion: { [weak self] _ in - self?.contentContainerNode.allowsGroupOpacity = false - }) - - if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { - let localSourceFrame = self.view.convert(CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalProjectedContentViewFrame.1.minY), size: CGSize(width: originalProjectedContentViewFrame.1.width, height: originalProjectedContentViewFrame.1.height)), to: self.scrollNode.view) - - self.contentContainerNode.layer.animateSpring(from: min(localSourceFrame.width / self.contentContainerNode.frame.width, localSourceFrame.height / self.contentContainerNode.frame.height) as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) - - switch self.legacySource { - case let .controller(controller): - controller.animatedIn() - default: - break - } - - let contentContainerOffset = CGPoint(x: localSourceFrame.center.x - self.contentContainerNode.frame.center.x, y: localSourceFrame.center.y - self.contentContainerNode.frame.center.y) - if let contentNode = self.contentContainerNode.contentNode, case let .controller(controller) = contentNode { - let snapshotView: UIView? = nil// controller.sourceNode.view.snapshotContentTree() - if let snapshotView = snapshotView { - controller.sourceView.isHidden = true - - self.view.insertSubview(snapshotView, belowSubview: self.contentContainerNode.view) - snapshotView.layer.animateSpring(from: NSValue(cgPoint: localSourceFrame.center), to: NSValue(cgPoint: CGPoint(x: self.contentContainerNode.frame.midX, y: self.contentContainerNode.frame.minY + localSourceFrame.height / 2.0)), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false) - //snapshotView.layer.animateSpring(from: 1.0 as NSNumber, to: (self.contentContainerNode.frame.width / localSourceFrame.width) as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false) - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - } - self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) - self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { [weak self] _ in - self?.animatedIn = true - }) - } - } - } - } - - private var delayLayoutUpdate = false - func animateOut(result initialResult: ContextMenuActionResult, completion: @escaping () -> Void) { - self.isUserInteractionEnabled = false - - self.beganAnimatingOut() - - if let sourceContainer = self.sourceContainer { - sourceContainer.animateOut(result: initialResult, completion: completion) - return - } - - var transitionDuration: Double = 0.2 - var transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut - - var result = initialResult - - switch self.legacySource { - case let .location(source): - let transitionInfo = source.transitionInfo() - if transitionInfo == nil { - result = .dismissWithoutContent - } - - switch result { - case let .custom(value): - switch value { - case let .animated(duration, curve): - transitionDuration = duration - transitionCurve = curve - default: - break - } - default: - break - } - - self.isUserInteractionEnabled = false - self.isAnimatingOut = true - - self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false) - - if !self.dimNode.isHidden { - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) - } else { - self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) - } - - self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15 * animationDurationFactor, removeOnCompletion: false, completion: { _ in - completion() - }) - self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) - - let animateOutToItem: Bool - switch result { - case .default, .custom: - animateOutToItem = true - case .dismissWithoutContent: - animateOutToItem = false - } - - if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { - let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) - self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) - } - case let .reference(source): - guard let maybeContentNode = self.contentContainerNode.contentNode, case let .reference(referenceView) = maybeContentNode else { - return - } - - let transitionInfo = source.transitionInfo() - if transitionInfo == nil { - result = .dismissWithoutContent - } - - switch result { - case let .custom(value): - switch value { - case let .animated(duration, curve): - transitionDuration = duration - transitionCurve = curve - default: - break - } - default: - break - } - - self.isUserInteractionEnabled = false - self.isAnimatingOut = true - - self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false) - - if let transitionInfo = transitionInfo, let parentSuperview = referenceView.superview { - self.originalProjectedContentViewFrame = (convertFrame(referenceView.frame, from: parentSuperview, to: self.view), convertFrame(referenceView.bounds, from: referenceView, to: self.view)) - - var updatedContentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace - updatedContentAreaInScreenSpace.origin.x = 0.0 - updatedContentAreaInScreenSpace.size.width = self.bounds.width - - self.clippingNode.layer.animateFrame(from: self.clippingNode.frame, to: updatedContentAreaInScreenSpace, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) - self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) - } - - if !self.dimNode.isHidden { - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) - } else { - self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) - } - - self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15 * animationDurationFactor, removeOnCompletion: false, completion: { _ in - completion() - }) - self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) - - let animateOutToItem: Bool - switch result { - case .default, .custom: - animateOutToItem = true - case .dismissWithoutContent: - animateOutToItem = false - } - - if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { - let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) - self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) - } - case let .extracted(source): - guard let maybeContentNode = self.contentContainerNode.contentNode, case let .extracted(contentParentNode, keepInPlace) = maybeContentNode else { - return - } - - let putBackInfo = source.putBack() - - if putBackInfo == nil { - result = .dismissWithoutContent - } - - switch result { - case let .custom(value): - switch value { - case let .animated(duration, curve): - transitionDuration = duration - transitionCurve = curve - default: - break - } - default: - break - } - - self.isUserInteractionEnabled = false - self.isAnimatingOut = true - - self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false) - - var completedEffect = false - var completedContentNode = false - var completedActionsNode = false - - if let putBackInfo = putBackInfo, let parentSupernode = contentParentNode.supernode { - self.originalProjectedContentViewFrame = (convertFrame(contentParentNode.frame, from: parentSupernode.view, to: self.view), convertFrame(contentParentNode.contentRect, from: contentParentNode.view, to: self.view)) - - var updatedContentAreaInScreenSpace = putBackInfo.contentAreaInScreenSpace - updatedContentAreaInScreenSpace.origin.x = 0.0 - updatedContentAreaInScreenSpace.size.width = self.bounds.width - - self.clippingNode.view.mask = putBackInfo.maskView - let previousFrame = self.clippingNode.frame - self.clippingNode.position = updatedContentAreaInScreenSpace.center - self.clippingNode.bounds = CGRect(origin: CGPoint(), size: updatedContentAreaInScreenSpace.size) - self.clippingNode.layer.animatePosition(from: previousFrame.center, to: updatedContentAreaInScreenSpace.center, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: true) - self.clippingNode.layer.animateBounds(from: CGRect(origin: CGPoint(), size: previousFrame.size), to: CGRect(origin: CGPoint(), size: updatedContentAreaInScreenSpace.size), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: true) - //self.clippingNode.layer.animateFrame(from: previousFrame, to: updatedContentAreaInScreenSpace, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) - //self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) - } - - let intermediateCompletion: () -> Void = { [weak self, weak contentParentNode] in - if completedEffect && completedContentNode && completedActionsNode { - switch result { - case .default, .custom: - if let contentParentNode = contentParentNode { - contentParentNode.addSubnode(contentParentNode.contentNode) - contentParentNode.isExtractedToContextPreview = false - contentParentNode.isExtractedToContextPreviewUpdated?(false) - } - case .dismissWithoutContent: - break - } - - self?.clippingNode.view.mask = nil - - completion() - } - } - - if #available(iOS 10.0, *) { - if let propertyAnimator = self.propertyAnimator { - let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator - propertyAnimator?.stopAnimation(true) - } - self.propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration * UIView.animationDurationFactor(), curve: .easeInOut, animations: { - //self?.effectView.effect = nil - }) - } - - if let _ = self.propertyAnimator { - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor * UIView.animationDurationFactor(), from: 0.0, to: 0.999, update: { [weak self] value in - (self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value - }, completion: { - completedEffect = true - intermediateCompletion() - }) - } - self.effectView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) - } else { - UIView.animate(withDuration: 0.21 * animationDurationFactor, animations: { - if #available(iOS 9.0, *) { - self.effectView.effect = nil - } else { - self.effectView.alpha = 0.0 - } - }, completion: { _ in - completedEffect = true - intermediateCompletion() - }) - } - - if !self.dimNode.isHidden { - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) - } else { - self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) - } - - self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15 * animationDurationFactor, removeOnCompletion: false, completion: { _ in - completedActionsNode = true - intermediateCompletion() - }) - self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) - - let animateOutToItem: Bool - switch result { - case .default, .custom: - animateOutToItem = true - case .dismissWithoutContent: - animateOutToItem = false - } - - if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { - let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) - let localContentSourceFrame: CGRect - if keepInPlace { - localContentSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.contentContainerNode.view.superview) - } else { - localContentSourceFrame = localSourceFrame - } - - var actionsOffset: CGFloat = 0.0 - if case let .extracted(source) = self.legacySource, source.centerVertically { - actionsOffset = -localSourceFrame.width * 0.6 - } - - self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y + actionsOffset), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) - let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY) - self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentContainerOffset, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true, completion: { _ in - completedContentNode = true - intermediateCompletion() - }) - contentParentNode.updateAbsoluteRect?(self.contentContainerNode.frame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y + contentContainerOffset.y), self.bounds.size) - contentParentNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: -contentContainerOffset.y), transitionCurve, transitionDuration) - - contentParentNode.willUpdateIsExtractedToContextPreview?(false, .animated(duration: 0.2, curve: .easeInOut)) - } else { - if let snapshotView = contentParentNode.contentNode.view.snapshotContentTree(keepTransform: true) { - self.contentContainerNode.view.addSubview(snapshotView) - } - - contentParentNode.addSubnode(contentParentNode.contentNode) - contentParentNode.isExtractedToContextPreview = false - contentParentNode.isExtractedToContextPreviewUpdated?(false) - - self.contentContainerNode.allowsGroupOpacity = true - self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false, completion: { _ in - completedContentNode = true - intermediateCompletion() - }) - - contentParentNode.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - contentParentNode.willUpdateIsExtractedToContextPreview?(false, .animated(duration: 0.2, curve: .easeInOut)) - } - case let .controller(source): - guard let maybeContentNode = self.contentContainerNode.contentNode, case let .controller(controller) = maybeContentNode else { - return - } - - let transitionInfo = source.transitionInfo() - - if transitionInfo == nil { - result = .dismissWithoutContent - } - - switch result { - case let .custom(value): - switch value { - case let .animated(duration, curve): - transitionDuration = duration - transitionCurve = curve - default: - break - } - default: - break - } - - self.isUserInteractionEnabled = false - self.isAnimatingOut = true - - self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false) - - var completedEffect = false - var completedContentNode = false - var completedActionsNode = false - - if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() { - let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view) - self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) - - var updatedContentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace - updatedContentAreaInScreenSpace.origin.x = 0.0 - updatedContentAreaInScreenSpace.size.width = self.bounds.width - } - - let intermediateCompletion: () -> Void = { - if completedEffect && completedContentNode && completedActionsNode { - switch result { - case .default, .custom: - break - case .dismissWithoutContent: - break - } - - completion() - } - } - - if #available(iOS 10.0, *) { - if let propertyAnimator = self.propertyAnimator { - let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator - propertyAnimator?.stopAnimation(true) - } - self.propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration * UIView.animationDurationFactor(), curve: .easeInOut, animations: { [weak self] in - self?.effectView.effect = nil - }) - } - - if let _ = self.propertyAnimator { - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor * UIView.animationDurationFactor(), from: 0.0, to: 0.999, update: { [weak self] value in - (self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value - }, completion: { - completedEffect = true - intermediateCompletion() - }) - } - self.effectView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.05 * animationDurationFactor, delay: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) - } else { - UIView.animate(withDuration: 0.21 * animationDurationFactor, animations: { - if #available(iOS 9.0, *) { - self.effectView.effect = nil - } else { - self.effectView.alpha = 0.0 - } - }, completion: { _ in - completedEffect = true - intermediateCompletion() - }) - } - - if !self.dimNode.isHidden { - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) - } else { - self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) - } - self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false, completion: { _ in - completedActionsNode = true - intermediateCompletion() - }) - self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false, completion: { _ in - }) - self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) - self.contentContainerNode.layer.animateScale(from: 1.0, to: 0.01, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) - - let animateOutToItem: Bool - switch result { - case .default, .custom: - animateOutToItem = true - case .dismissWithoutContent: - animateOutToItem = false - } - - if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { - let localSourceFrame = self.view.convert(CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalProjectedContentViewFrame.1.minY), size: CGSize(width: originalProjectedContentViewFrame.1.width, height: originalProjectedContentViewFrame.1.height)), to: self.scrollNode.view) - - self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) - let contentContainerOffset = CGPoint(x: localSourceFrame.center.x - self.contentContainerNode.frame.center.x, y: localSourceFrame.center.y - self.contentContainerNode.frame.center.y) - self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentContainerOffset, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true, completion: { [weak self] _ in - completedContentNode = true - if let strongSelf = self, let contentNode = strongSelf.contentContainerNode.contentNode, case let .controller(controller) = contentNode { - controller.sourceView.isHidden = false - } - intermediateCompletion() - }) - } else { - if let contentNode = self.contentContainerNode.contentNode, case let .controller(controller) = contentNode { - controller.sourceView.isHidden = false - } - - if let snapshotView = controller.view.snapshotContentTree(keepTransform: true) { - self.contentContainerNode.view.addSubview(snapshotView) - } - - self.contentContainerNode.allowsGroupOpacity = true - self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false, completion: { _ in - completedContentNode = true - intermediateCompletion() - }) - } - } - } - - func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { - if let sourceContainer = self.sourceContainer { - sourceContainer.addRelativeContentOffset(offset, transition: transition) - } - } - - func cancelReactionAnimation() { - if let sourceContainer = self.sourceContainer { - sourceContainer.cancelReactionAnimation() - } - } - - func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: @escaping () -> Void) { - if let sourceContainer = self.sourceContainer { - sourceContainer.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, onHit: onHit, completion: completion) - } - } - - func animateDismissalIfNeeded() { - guard let layout = self.validLayout, layout.metrics.isTablet else { - return - } - if let sourceContainer = self.sourceContainer { - sourceContainer.animateOut(result: .dismissWithoutContent, completion: {}) - return - } - } - - func getActionsMinHeight() -> ContextController.ActionsHeight? { - if !self.actionsContainerNode.bounds.height.isZero { - return ContextController.ActionsHeight( - minY: self.actionsContainerNode.frame.minY, - contentOffset: self.scrollNode.view.contentOffset.y - ) - } else { - return nil - } - } - - func setItemsSignal(items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition, animated: Bool) { - if let sourceContainer = self.sourceContainer { - sourceContainer.setItems(items: items, animated: animated) - } else { - self.legacyItems = items - self.itemsDisposable.set((items - |> deliverOnMainQueue).start(next: { [weak self] items in - guard let strongSelf = self else { - return - } - strongSelf.setItems(items: items, minHeight: minHeight, previousActionsTransition: previousActionsTransition) - })) - } - } - - private func setItems(items: ContextController.Items, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { - if let sourceContainer = self.sourceContainer { - let disableAnimations = self.getController()?.immediateItemsTransitionAnimation == true - sourceContainer.setItems(items: .single(items), animated: !disableAnimations) - - if !self.didSetItemsReady { - self.didSetItemsReady = true - self.itemsReady.set(.single(true)) - } - return - } - - if let _ = self.currentItems, !self.didCompleteAnimationIn && self.getController()?.immediateItemsTransitionAnimation == true { - return - } - - self.currentItems = items - self.currentActionsMinHeight = minHeight - - let previousActionsContainerNode = self.actionsContainerNode - let previousActionsContainerFrame = previousActionsContainerNode.view.convert(previousActionsContainerNode.bounds, to: self.view) - self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: items, getController: { [weak self] in - return self?.getController() - }, actionSelected: { [weak self] result in - self?.beginDismiss(result) - }, requestLayout: { [weak self] in - self?.updateLayout() - }, feedbackTap: { [weak self] in - self?.hapticFeedback.tap() - }, blurBackground: self.blurBackground) - self.scrollNode.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode) - - if let layout = self.validLayout { - self.updateLayout(layout: layout, transition: self.didSetItemsReady ? .animated(duration: 0.3, curve: .spring) : .immediate, previousActionsContainerNode: previousActionsContainerNode, previousActionsContainerFrame: previousActionsContainerFrame, previousActionsTransition: previousActionsTransition) - } else { - previousActionsContainerNode.removeFromSupernode() - } - - if !self.didSetItemsReady { - self.didSetItemsReady = true - self.itemsReady.set(.single(true)) - } - } - - func pushItems(items: Signal) { - if let sourceContainer = self.sourceContainer { - sourceContainer.pushItems(items: items) - } - } - - func popItems() { - if let sourceContainer = self.sourceContainer { - sourceContainer.popItems() - } - } - - func updateTheme(presentationData: PresentationData) { - self.presentationData = presentationData - - self.dimNode.backgroundColor = presentationData.theme.contextMenu.dimColor - self.actionsContainerNode.updateTheme(presentationData: presentationData) - - if let validLayout = self.validLayout { - self.updateLayout(layout: validLayout, transition: .immediate, previousActionsContainerNode: nil, previousActionsContainerFrame: nil) - } - } - - func updateLayout() { - if let layout = self.validLayout { - self.updateLayout(layout: layout, transition: .immediate, previousActionsContainerNode: nil) - } - } - - func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?, previousActionsContainerFrame: CGRect? = nil, previousActionsTransition: ContextController.PreviousActionsTransition = .scale) { - if self.isAnimatingOut || self.delayLayoutUpdate { - return - } - - self.validLayout = layout - - if let sourceContainer = self.sourceContainer { - transition.updateFrame(node: sourceContainer, frame: CGRect(origin: CGPoint(), size: layout.size)) - sourceContainer.update( - presentationData: self.presentationData, - layout: layout, - transition: transition - ) - return - } - - var actionsContainerTransition = transition - if previousActionsContainerNode != nil { - actionsContainerTransition = .immediate - } - - transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.withoutBlurDimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - switch layout.metrics.widthClass { - case .compact: - if case .reference = self.legacySource { - } else if case let .extracted(extractedSource) = self.legacySource, !extractedSource.blurBackground { - } else if self.effectView.superview == nil { - self.view.insertSubview(self.effectView, at: 0) - if #available(iOS 10.0, *) { - if let propertyAnimator = self.propertyAnimator { - let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator - propertyAnimator?.stopAnimation(true) - } - } - self.effectView.effect = makeCustomZoomBlurEffect(isLight: presentationData.theme.rootController.keyboardColor == .light) - self.dimNode.alpha = 1.0 - } - self.dimNode.isHidden = false - self.withoutBlurDimNode.isHidden = true - case .regular: - if case .reference = self.legacySource { - } else if case let .extracted(extractedSource) = self.legacySource, !extractedSource.blurBackground { - } else if self.effectView.superview != nil { - self.effectView.removeFromSuperview() - self.withoutBlurDimNode.alpha = 1.0 - } - self.dimNode.isHidden = true - self.withoutBlurDimNode.isHidden = false - } - transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - let actionsSideInset: CGFloat = layout.safeInsets.left + 12.0 - let contentTopInset: CGFloat = max(11.0, layout.statusBarHeight ?? 0.0) - - let actionsBottomInset: CGFloat = 11.0 - - if let contentNode = self.contentContainerNode.contentNode { - switch contentNode { - case let .reference(referenceNode): - let contentActionsSpacing: CGFloat = 8.0 - if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { - let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero - let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) - - let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, presentation: .inline, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: layout.size.height, transition: actionsContainerTransition) - let adjustedActionsSize = realActionsSize - - self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize) - let contentSize = originalProjectedContentViewFrame.1.size - self.contentContainerNode.updateLayout(size: contentSize, scaledSize: contentSize, transition: transition) - - let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - adjustedActionsSize.height) - - let originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin) - let preferredActionsX = originalProjectedContentViewFrame.1.minX - - var originalActionsFrame = CGRect(origin: CGPoint(x: max(actionsSideInset, min(layout.size.width - adjustedActionsSize.width - actionsSideInset, preferredActionsX)), y: originalActionsY), size: realActionsSize) - let originalContentX: CGFloat = originalProjectedContentViewFrame.1.minX - let originalContentY = originalProjectedContentViewFrame.1.minY - - var originalContentFrame = CGRect(origin: CGPoint(x: originalContentX, y: originalContentY), size: originalProjectedContentViewFrame.1.size) - let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0) - let bottomEdge = min(layout.size.height - layout.intrinsicInsets.bottom, self.contentAreaInScreenSpace?.maxY ?? layout.size.height) - - if originalContentFrame.minY < topEdge { - let requiredOffset = topEdge - originalContentFrame.minY - let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY) - let offset = min(requiredOffset, availableOffset) - originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) - originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) - } else if originalActionsFrame.maxY > bottomEdge { - let requiredOffset = bottomEdge - originalActionsFrame.maxY - let offset = requiredOffset - originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) - originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) - } - - var contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset) - contentHeight = max(contentHeight, adjustedActionsSize.height + originalActionsFrame.minY + actionsBottomInset) - - var overflowOffset: CGFloat - var contentContainerFrame: CGRect - - overflowOffset = min(0.0, originalActionsFrame.minY - contentTopInset) - let contentParentNode = referenceNode - contentContainerFrame = originalContentFrame - if !overflowOffset.isZero { - let offsetDelta = contentParentNode.frame.height + 4.0 - overflowOffset += offsetDelta - overflowOffset = min(0.0, overflowOffset) - - originalActionsFrame.origin.x -= contentParentNode.frame.width + 14.0 - originalActionsFrame.origin.x = max(actionsSideInset, originalActionsFrame.origin.x) - - if originalActionsFrame.minX < contentContainerFrame.minX { - contentContainerFrame.origin.x = min(originalActionsFrame.maxX + 14.0, layout.size.width - actionsSideInset) - } - originalActionsFrame.origin.y += offsetDelta - if originalActionsFrame.maxY < originalContentFrame.maxY { - originalActionsFrame.origin.y += contentParentNode.frame.height - originalActionsFrame.origin.y = min(originalActionsFrame.origin.y, layout.size.height - originalActionsFrame.height - actionsBottomInset) - } - contentHeight -= offsetDelta - } - - if let customPosition = self.customPosition { - originalActionsFrame.origin.x = floor(originalContentFrame.center.x - originalActionsFrame.width / 2.0) + customPosition.x - originalActionsFrame.origin.y = floor(originalContentFrame.center.y - originalActionsFrame.height / 2.0) + customPosition.y - } - - let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) - if self.scrollNode.view.contentSize != scrollContentSize { - self.scrollNode.view.contentSize = scrollContentSize - } - self.actionsContainerNode.panSelectionGestureEnabled = scrollContentSize.height <= layout.size.height - - transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) - actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) - - if isInitialLayout { - let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) - if overflowOffset < 0.0 { - transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY) - } - } - } - case let .extracted(contentParentNode, keepInPlace): - var centerVertically = false - if case let .extracted(source) = self.legacySource, source.centerVertically { - centerVertically = true - } - let contentActionsSpacing: CGFloat = keepInPlace ? 16.0 : 8.0 - if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { - let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero - let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) - - let constrainedActionsHeight: CGFloat - let constrainedActionsBottomInset: CGFloat - if let currentActionsMinHeight = self.currentActionsMinHeight { - constrainedActionsBottomInset = actionsBottomInset + layout.intrinsicInsets.bottom - constrainedActionsHeight = layout.size.height - currentActionsMinHeight.minY - constrainedActionsBottomInset - } else { - constrainedActionsHeight = layout.size.height - constrainedActionsBottomInset = 0.0 - } - - let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, presentation: .inline, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: constrainedActionsHeight, transition: actionsContainerTransition) - let adjustedActionsSize = realActionsSize - - self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize) - let contentSize = originalProjectedContentViewFrame.1.size - self.contentContainerNode.updateLayout(size: contentSize, scaledSize: contentSize, transition: transition) - - let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - adjustedActionsSize.height) - let preferredActionsX: CGFloat - var originalActionsY: CGFloat - if centerVertically { - originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin) - preferredActionsX = originalProjectedContentViewFrame.1.maxX - adjustedActionsSize.width - } else if keepInPlace { - originalActionsY = originalProjectedContentViewFrame.1.minY - contentActionsSpacing - adjustedActionsSize.height - preferredActionsX = max(actionsSideInset, originalProjectedContentViewFrame.1.maxX - adjustedActionsSize.width) - } else { - originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin) - preferredActionsX = originalProjectedContentViewFrame.1.minX - } - - if let currentActionsMinHeight = self.currentActionsMinHeight { - originalActionsY = currentActionsMinHeight.minY - } - - var originalActionsFrame = CGRect(origin: CGPoint(x: max(actionsSideInset, min(layout.size.width - adjustedActionsSize.width - actionsSideInset, preferredActionsX)), y: originalActionsY), size: realActionsSize) - let originalContentX: CGFloat = originalProjectedContentViewFrame.1.minX - let originalContentY: CGFloat - if keepInPlace { - originalContentY = originalProjectedContentViewFrame.1.minY - } else { - originalContentY = originalActionsFrame.minY - contentActionsSpacing - originalProjectedContentViewFrame.1.size.height - } - var originalContentFrame = CGRect(origin: CGPoint(x: originalContentX, y: originalContentY), size: originalProjectedContentViewFrame.1.size) - let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0) - if originalContentFrame.minY < topEdge { - let requiredOffset = topEdge - originalContentFrame.minY - let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY) - let offset = min(requiredOffset, availableOffset) - originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) - originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) - } - - var contentHeight: CGFloat - if keepInPlace { - contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalActionsFrame.minY + contentTopInset) - } else { - if self.currentActionsMinHeight != nil { - contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset + layout.intrinsicInsets.bottom)) - } else { - contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset + layout.intrinsicInsets.bottom) - originalContentFrame.minY + contentTopInset) - } - } - - var overflowOffset: CGFloat - var contentContainerFrame: CGRect - if centerVertically { - overflowOffset = 0.0 - if layout.size.width > layout.size.height, case .compact = layout.metrics.widthClass { - let totalWidth = originalContentFrame.width + originalActionsFrame.width + contentActionsSpacing - contentContainerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - totalWidth) / 2.0 + originalContentFrame.width * 0.1), y: floor((layout.size.height - originalContentFrame.height) / 2.0)), size: originalContentFrame.size) - originalActionsFrame.origin.x = contentContainerFrame.maxX + contentActionsSpacing + 14.0 - originalActionsFrame.origin.y = contentContainerFrame.origin.y - contentHeight = layout.size.height - } else { - let totalHeight = originalContentFrame.height + originalActionsFrame.height - contentContainerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - originalContentFrame.width) / 2.0), y: floor((layout.size.height - totalHeight) / 2.0)), size: originalContentFrame.size) - originalActionsFrame.origin.y = contentContainerFrame.maxY + contentActionsSpacing - } - } else if keepInPlace { - overflowOffset = min(0.0, originalActionsFrame.minY - contentTopInset) - contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -contentParentNode.contentRect.minY) - if !overflowOffset.isZero { - let offsetDelta = contentParentNode.contentRect.height + 4.0 - overflowOffset += offsetDelta - overflowOffset = min(0.0, overflowOffset) - - originalActionsFrame.origin.x -= contentParentNode.contentRect.maxX - contentParentNode.contentRect.minX + 14.0 - originalActionsFrame.origin.x = max(actionsSideInset, originalActionsFrame.origin.x) - //originalActionsFrame.origin.y += contentParentNode.contentRect.height - if originalActionsFrame.minX < contentContainerFrame.minX { - contentContainerFrame.origin.x = min(originalActionsFrame.maxX + 14.0, layout.size.width - actionsSideInset) - } - originalActionsFrame.origin.y += offsetDelta - if originalActionsFrame.maxY < originalContentFrame.maxY { - originalActionsFrame.origin.y += contentParentNode.contentRect.height - originalActionsFrame.origin.y = min(originalActionsFrame.origin.y, layout.size.height - originalActionsFrame.height - actionsBottomInset) - } - contentHeight -= offsetDelta - } - } else { - overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) - contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -overflowOffset - contentParentNode.contentRect.minY) - - if contentContainerFrame.maxX > layout.size.width { - contentContainerFrame = CGRect(origin: CGPoint(x: layout.size.width - contentContainerFrame.width - 11.0, y: contentContainerFrame.minY), size: contentContainerFrame.size) - } - } - - let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) - if self.scrollNode.view.contentSize != scrollContentSize { - self.scrollNode.view.contentSize = scrollContentSize - } - self.actionsContainerNode.panSelectionGestureEnabled = scrollContentSize.height <= layout.size.height - - transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) - actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) - - if isInitialLayout { - //let previousContentOffset = self.scrollNode.view.contentOffset.y - if !keepInPlace { - if let currentActionsMinHeight = self.currentActionsMinHeight { - self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: currentActionsMinHeight.contentOffset) - } else { - self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset) - } - } - let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) - var offset: CGFloat = 0.0 - //offset -= previousContentOffset - self.scrollNode.view.contentOffset.y - offset += previousContainerFrame.minY - currentContainerFrame.minY - transition.animatePositionAdditive(node: self.contentContainerNode, offset: CGPoint(x: 0.0, y: offset)) - if overflowOffset < 0.0 { - let _ = currentContainerFrame - let _ = previousContainerFrame - } - } - - let absoluteContentRect = contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y) - - contentParentNode.updateAbsoluteRect?(absoluteContentRect, layout.size) - } - case let .controller(contentParentNode): - var projectedFrame: CGRect = convertFrame(contentParentNode.sourceView.bounds, from: contentParentNode.sourceView, to: self.view) - switch self.legacySource { - case let .controller(source): - let transitionInfo = source.transitionInfo() - if let (sourceView, sourceRect) = transitionInfo?.sourceNode() { - projectedFrame = convertFrame(sourceRect, from: sourceView, to: self.view) - } - default: - break - } - self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) - - if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { - let contentActionsSpacing: CGFloat = actionsSideInset - let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0) - - let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero - let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) - - let constrainedWidth: CGFloat - if layout.size.width < layout.size.height { - constrainedWidth = layout.size.width - } else { - constrainedWidth = floor(layout.size.width / 2.0) - } - - let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, presentation: .inline, constrainedWidth: constrainedWidth - actionsSideInset * 2.0, constrainedHeight: layout.size.height, transition: actionsContainerTransition) - let contentScale = (constrainedWidth - actionsSideInset * 2.0) / constrainedWidth - var contentUnscaledSize: CGSize - if case .compact = layout.metrics.widthClass { - self.actionsContainerNode.updateSize(containerSize: actionsSize, contentSize: actionsSize) - - let proposedContentHeight: CGFloat - if layout.size.width < layout.size.height { - proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset - } else { - proposedContentHeight = layout.size.height - topEdge - topEdge - - let maxActionsHeight = layout.size.height - topEdge - topEdge - self.actionsContainerNode.updateSize(containerSize: CGSize(width: actionsSize.width, height: min(actionsSize.height, maxActionsHeight)), contentSize: actionsSize) - } - contentUnscaledSize = CGSize(width: constrainedWidth, height: max(100.0, proposedContentHeight)) - - if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { - contentUnscaledSize = preferredSize - } - } else { - let maxActionsHeight = layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height - self.actionsContainerNode.updateSize(containerSize: CGSize(width: actionsSize.width, height: min(actionsSize.height, maxActionsHeight)), contentSize: actionsSize) - - let proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset - contentUnscaledSize = CGSize(width: min(layout.size.width, 340.0), height: min(568.0, proposedContentHeight)) - - if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { - contentUnscaledSize = preferredSize - } - } - let contentSize = CGSize(width: floor(contentUnscaledSize.width * contentScale), height: floor(contentUnscaledSize.height * contentScale)) - - self.contentContainerNode.updateLayout(size: contentUnscaledSize, scaledSize: contentSize, transition: transition) - - let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height) - var originalActionsFrame: CGRect - var originalContentFrame: CGRect - var contentHeight: CGFloat - if case .compact = layout.metrics.widthClass { - if layout.size.width < layout.size.height { - let sideInset = floor((layout.size.width - max(contentSize.width, actionsSize.width)) / 2.0) - originalActionsFrame = CGRect(origin: CGPoint(x: sideInset, y: min(maximumActionsFrameOrigin, floor((layout.size.height - contentActionsSpacing - contentSize.height) / 2.0) + contentSize.height + contentActionsSpacing)), size: actionsSize) - originalContentFrame = CGRect(origin: CGPoint(x: sideInset, y: originalActionsFrame.minY - contentActionsSpacing - contentSize.height), size: contentSize) - if originalContentFrame.minY < topEdge { - let requiredOffset = topEdge - originalContentFrame.minY - let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY) - let offset = min(requiredOffset, availableOffset) - originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) - originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) - } - contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset) - } else { - originalContentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width - actionsSideInset - actionsSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) - originalActionsFrame = CGRect(origin: CGPoint(x: originalContentFrame.maxX + actionsSideInset, y: max(topEdge, originalContentFrame.minY)), size: actionsSize) - contentHeight = max(layout.size.height, max(originalContentFrame.maxY, originalActionsFrame.maxY)) - } - } else { - originalContentFrame = CGRect(origin: CGPoint(x: floor(originalProjectedContentViewFrame.1.midX - contentSize.width / 2.0), y: floor(originalProjectedContentViewFrame.1.midY - contentSize.height / 2.0)), size: contentSize) - originalContentFrame.origin.x = min(originalContentFrame.origin.x, layout.size.width - actionsSideInset - contentSize.width) - originalContentFrame.origin.x = max(originalContentFrame.origin.x, actionsSideInset) - originalContentFrame.origin.y = min(originalContentFrame.origin.y, layout.size.height - layout.intrinsicInsets.bottom - actionsSideInset - contentSize.height) - originalContentFrame.origin.y = max(originalContentFrame.origin.y, contentTopInset) - if originalContentFrame.maxX <= layout.size.width - actionsSideInset - actionsSize.width - contentActionsSpacing { - originalActionsFrame = CGRect(origin: CGPoint(x: originalContentFrame.maxX + contentActionsSpacing, y: originalContentFrame.minY), size: actionsSize) - if originalActionsFrame.maxX > layout.size.width - actionsSideInset { - let offset = originalActionsFrame.maxX - (layout.size.width - actionsSideInset) - originalActionsFrame.origin.x -= offset - originalContentFrame.origin.x -= offset - } - } else { - originalActionsFrame = CGRect(origin: CGPoint(x: originalContentFrame.minX - contentActionsSpacing - actionsSize.width, y: originalContentFrame.minY), size: actionsSize) - if originalActionsFrame.minX < actionsSideInset { - let offset = actionsSideInset - originalActionsFrame.minX - originalActionsFrame.origin.x += offset - originalContentFrame.origin.x += offset - } - } - contentHeight = layout.size.height - contentHeight = max(contentHeight, originalActionsFrame.maxY + actionsBottomInset) - contentHeight = max(contentHeight, originalContentFrame.maxY + actionsBottomInset) - } - - let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) - if self.scrollNode.view.contentSize != scrollContentSize { - self.scrollNode.view.contentSize = scrollContentSize - } - self.actionsContainerNode.panSelectionGestureEnabled = true - - let overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) - - let contentContainerFrame = originalContentFrame - transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) - actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) - - if isInitialLayout { - self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset) - let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) - if overflowOffset < 0.0 { - transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY) - } - } - } - } - } - - if let previousActionsContainerNode = previousActionsContainerNode { - if transition.isAnimated && self.getController()?.immediateItemsTransitionAnimation == false { - if previousActionsContainerNode.hasAdditionalActions && !self.actionsContainerNode.hasAdditionalActions && self.getController()?.useComplexItemsTransitionAnimation == true { - var initialFrame = self.actionsContainerNode.frame - let delta = (previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height) - initialFrame.origin.y = self.actionsContainerNode.frame.minY + previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height - transition.animateFrame(node: self.actionsContainerNode, from: initialFrame) - transition.animatePosition(node: previousActionsContainerNode, to: CGPoint(x: 0.0, y: -delta), removeOnCompletion: false, additive: true) - previousActionsContainerNode.animateOut(offset: delta, transition: transition) - - previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in - previousActionsContainerNode?.removeFromSupernode() - }) - self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } else { - if let previousActionsContainerFrame = previousActionsContainerFrame { - previousActionsContainerNode.frame = self.view.convert(previousActionsContainerFrame, to: self.actionsContainerNode.view.superview!) - } - - switch previousActionsTransition { - case .scale: - transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.1) - previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in - previousActionsContainerNode?.removeFromSupernode() - }) - - transition.animateTransformScale(node: self.actionsContainerNode, from: 0.1) - self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - case let .slide(forward): - let deltaY = self.actionsContainerNode.frame.minY - previousActionsContainerNode.frame.minY - var previousNodePosition = previousActionsContainerNode.position.offsetBy(dx: 0.0, dy: deltaY) - let additionalHorizontalOffset: CGFloat = 20.0 - let currentNodeOffset: CGFloat - if forward { - previousNodePosition = previousNodePosition.offsetBy(dx: -previousActionsContainerNode.frame.width / 2.0 - additionalHorizontalOffset, dy: -previousActionsContainerNode.frame.height / 2.0) - currentNodeOffset = self.actionsContainerNode.bounds.width / 2.0 + additionalHorizontalOffset - } else { - previousNodePosition = previousNodePosition.offsetBy(dx: previousActionsContainerNode.frame.width / 2.0 + additionalHorizontalOffset, dy: -previousActionsContainerNode.frame.height / 2.0) - currentNodeOffset = -self.actionsContainerNode.bounds.width / 2.0 - additionalHorizontalOffset - } - transition.updatePosition(node: previousActionsContainerNode, position: previousNodePosition) - transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.01) - previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in - previousActionsContainerNode?.removeFromSupernode() - }) - - transition.animatePositionAdditive(node: self.actionsContainerNode, offset: CGPoint(x: currentNodeOffset, y: -deltaY - self.actionsContainerNode.bounds.height / 2.0)) - transition.animateTransformScale(node: self.actionsContainerNode, from: 0.01) - self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - } - } else { - previousActionsContainerNode.removeFromSupernode() - } - } - - transition.updateFrame(node: self.dismissNode, frame: CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize)) - self.dismissAccessibilityArea.frame = CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize) - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - guard let layout = self.validLayout else { - return - } - if let maybeContentNode = self.contentContainerNode.contentNode, case let .extracted(contentParentNode, keepInPlace) = maybeContentNode { - let contentContainerFrame = self.contentContainerNode.frame - let absoluteRect: CGRect - if keepInPlace { - absoluteRect = contentContainerFrame - } else { - absoluteRect = contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y) - } - contentParentNode.updateAbsoluteRect?(absoluteRect, layout.size) - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.bounds.contains(point) { - return nil - } - if !self.isUserInteractionEnabled { - return nil - } - - if let controller = self.getController() as? ContextController { - var innerResult: UIView? - controller.forEachController { c in - if let c = c as? UndoOverlayController { - if let result = c.view.hitTest(self.view.convert(point, to: c.view), with: event) { - innerResult = result - return false - } - } - return true - } - if let innerResult = innerResult { - return innerResult - } - } - - if let sourceContainer = self.sourceContainer { - return sourceContainer.hitTest(self.view.convert(point, to: sourceContainer.view), with: event) - } - - let mappedPoint = self.view.convert(point, to: self.scrollNode.view) - var maybePassthrough: ContextController.HandledTouchEvent? - if let maybeContentNode = self.contentContainerNode.contentNode { - switch maybeContentNode { - case .reference: - if let controller = self.getController() as? ContextController, let passthroughTouchEvent = controller.passthroughTouchEvent { - maybePassthrough = passthroughTouchEvent(self.view, point) - } - case let .extracted(contentParentNode, _): - if case let .extracted(source) = self.legacySource { - if !source.ignoreContentTouches { - let contentPoint = self.view.convert(point, to: contentParentNode.contentNode.view) - if let result = contentParentNode.contentNode.customHitTest?(contentPoint) { - return result - } else if let result = contentParentNode.contentNode.hitTest(contentPoint, with: event) { - if result is TextSelectionNodeView { - return result - } else if contentParentNode.contentRect.contains(contentPoint) { - return contentParentNode.contentNode.view - } - } - } - } - case let .controller(controller): - var passthrough = false - switch self.legacySource { - case let .controller(controllerSource): - passthrough = controllerSource.passthroughTouches - default: - break - } - if passthrough { - let controllerPoint = self.view.convert(point, to: controller.controller.view) - if let result = controller.controller.view.hitTest(controllerPoint, with: event) { - return result - } - } - } - } - - if self.actionsContainerNode.frame.contains(mappedPoint) { - return self.actionsContainerNode.hitTest(self.view.convert(point, to: self.actionsContainerNode.view), with: event) - } - - if let maybePassthrough = maybePassthrough { - switch maybePassthrough { - case .ignore: - break - case let .dismiss(consume, hitTestResult): - self.getController()?.dismiss(completion: nil) - - if let hitTestResult = hitTestResult { - return hitTestResult - } - if !consume { - return nil - } - } - } - - return self.dismissNode.view - } - - fileprivate func performHighlightedAction() { - self.sourceContainer?.performHighlightedAction() - } - - fileprivate func decreaseHighlightedIndex() { - self.sourceContainer?.decreaseHighlightedIndex() - } - - fileprivate func increaseHighlightedIndex() { - self.sourceContainer?.increaseHighlightedIndex() - } -} - public final class ContextControllerLocationViewInfo { public let location: CGPoint public let contentAreaInScreenSpace: CGRect @@ -2148,9 +314,7 @@ public final class ContextControllerReferenceViewInfo { public protocol ContextReferenceContentSource: AnyObject { var keepInPlace: Bool { get } - var shouldBeDismissed: Signal { get } - var forceDisplayBelowKeyboard: Bool { get } func transitionInfo() -> ContextControllerReferenceViewInfo? @@ -2296,587 +460,502 @@ public protocol ContextControllerItemsContent: AnyObject { ) -> ContextControllerItemsNode } -public final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol, KeyShortcutResponder { - public final class Source { - public let id: AnyHashable - public let title: String - public let footer: String? - public let source: ContextContentSource - public let items: Signal - public let closeActionTitle: String? - public let closeAction: (() -> Void)? - - public init( - id: AnyHashable, - title: String, - footer: String? = nil, - source: ContextContentSource, - items: Signal, - closeActionTitle: String? = nil, - closeAction: (() -> Void)? = nil - ) { - self.id = id - self.title = title - self.footer = footer - self.source = source - self.items = items - self.closeActionTitle = closeActionTitle - self.closeAction = closeAction - } - } - - public final class Configuration { - public let sources: [Source] - public let initialId: AnyHashable - - public init(sources: [Source], initialId: AnyHashable) { - self.sources = sources - self.initialId = initialId - } - } - - public struct Items { - public enum Content { - case list([ContextMenuItem]) - case twoLists([ContextMenuItem], [ContextMenuItem]) - case custom(ContextControllerItemsContent) - } - - public var id: AnyHashable? - public var content: Content - public var context: AccountContext? - public var reactionItems: [ReactionContextItem] - public var selectedReactionItems: Set - public var reactionsTitle: String? - public var reactionsLocked: Bool - public var animationCache: AnimationCache? - public var alwaysAllowPremiumReactions: Bool - public var allPresetReactionsAreAvailable: Bool - public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? - public var disablePositionLock: Bool - public var previewReaction: TelegramMediaFile? - public var tip: Tip? - public var tipSignal: Signal? - public var dismissed: (() -> Void)? - - public init( - id: AnyHashable? = nil, - content: Content, - context: AccountContext? = nil, - reactionItems: [ReactionContextItem] = [], - selectedReactionItems: Set = Set(), - reactionsTitle: String? = nil, - reactionsLocked: Bool = false, - animationCache: AnimationCache? = nil, - alwaysAllowPremiumReactions: Bool = false, - allPresetReactionsAreAvailable: Bool = false, - getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? = nil, - disablePositionLock: Bool = false, - previewReaction: TelegramMediaFile? = nil, - tip: Tip? = nil, - tipSignal: Signal? = nil, - dismissed: (() -> Void)? = nil - ) { - self.id = id - self.content = content - self.context = context - self.animationCache = animationCache - self.reactionItems = reactionItems - self.selectedReactionItems = selectedReactionItems - self.reactionsTitle = reactionsTitle - self.reactionsLocked = reactionsLocked - self.alwaysAllowPremiumReactions = alwaysAllowPremiumReactions - self.allPresetReactionsAreAvailable = allPresetReactionsAreAvailable - self.getEmojiContent = getEmojiContent - self.disablePositionLock = disablePositionLock - self.previewReaction = previewReaction - self.tip = tip - self.tipSignal = tipSignal - self.dismissed = dismissed - } - - public init() { - self.id = nil - self.content = .list([]) - self.context = nil - self.reactionItems = [] - self.selectedReactionItems = Set() - self.reactionsTitle = nil - self.reactionsLocked = false - self.alwaysAllowPremiumReactions = false - self.allPresetReactionsAreAvailable = false - self.getEmojiContent = nil - self.disablePositionLock = false - self.previewReaction = nil - self.tip = nil - self.tipSignal = nil - self.dismissed = nil - } - } - - public enum PreviousActionsTransition { - case scale - case slide(forward: Bool) - } - - public enum Tip: Equatable { - case textSelection - case quoteSelection - case messageViewsPrivacy - case messageCopyProtection(isChannel: Bool) - case animatedEmoji(text: String?, arguments: TextNodeWithEntities.Arguments?, file: TelegramMediaFile?, action: (() -> Void)?) - case notificationTopicExceptions(text: String, action: (() -> Void)?) - case starsReactions(topCount: Int) - case videoProcessing - case collageReordering - - public static func ==(lhs: Tip, rhs: Tip) -> Bool { - switch lhs { - case .textSelection: - if case .textSelection = rhs { - return true - } else { - return false - } - case .quoteSelection: - if case .quoteSelection = rhs { - return true - } else { - return false - } - case .messageViewsPrivacy: - if case .messageViewsPrivacy = rhs { - return true - } else { - return false - } - case let .messageCopyProtection(isChannel): - if case .messageCopyProtection(isChannel) = rhs { - return true - } else { - return false - } - case let .animatedEmoji(text, _, file, _): - if case let .animatedEmoji(rhsText, _, rhsFile, _) = rhs { - if text != rhsText { - return false - } - if file?.fileId != rhsFile?.fileId { - return false - } - return true - } else { - return false - } - case let .notificationTopicExceptions(text, _): - if case .notificationTopicExceptions(text, _) = rhs { - return true - } else { - return false - } - case let .starsReactions(topCount): - if case .starsReactions(topCount) = rhs { - return true - } else { - return false - } - case .videoProcessing: - if case .videoProcessing = rhs { - return true - } else { - return false - } - case .collageReordering: - if case .collageReordering = rhs { - return true - } else { - return false - } - } - } - } - - public final class ActionsHeight { - fileprivate let minY: CGFloat - fileprivate let contentOffset: CGFloat - - fileprivate init(minY: CGFloat, contentOffset: CGFloat) { - self.minY = minY - self.contentOffset = contentOffset - } - } - - private let context: AccountContext? - private var presentationData: PresentationData - private let configuration: ContextController.Configuration - - private let _ready = Promise() - override public var ready: Promise { - return self._ready - } - - private weak var recognizer: TapLongTapOrDoubleTapGestureRecognizer? - private weak var gesture: ContextGesture? - - private var animatedDidAppear = false - private var wasDismissed = false - private var dismissOnInputClose: (result: ContextMenuActionResult, completion: (() -> Void)?)? - private var dismissToReactionOnInputClose: (value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: (() -> Void)?)? - - override public var overlayWantsToBeBelowKeyboard: Bool { - if self.isNodeLoaded { - return self.controllerNode.overlayWantsToBeBelowKeyboard - } else { - return false - } - } - - var controllerNode: ContextControllerNode { - return self.displayNode as! ContextControllerNode - } - - public var dismissed: (() -> Void)? - public var dismissedForCancel: (() -> Void)? { - didSet { - self.controllerNode.dismissedForCancel = self.dismissedForCancel - } - } - - public var useComplexItemsTransitionAnimation = false - public var immediateItemsTransitionAnimation = false - let workaroundUseLegacyImplementation: Bool - let disableScreenshots: Bool - let hideReactionPanelTail: Bool - - public enum HandledTouchEvent { - case ignore - case dismiss(consume: Bool, result: UIView?) - } - - public var passthroughTouchEvent: ((UIView, CGPoint) -> HandledTouchEvent)? - - private var shouldBeDismissedDisposable: Disposable? - - public var reactionSelected: ((UpdateMessageReaction, Bool) -> Void)? - public var premiumReactionsSelected: (() -> Void)? - - public var getOverlayViews: (() -> [UIView])? - - convenience public init(context: AccountContext? = nil, presentationData: PresentationData, source: ContextContentSource, items: Signal, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, workaroundUseLegacyImplementation: Bool = false, disableScreenshots: Bool = false, hideReactionPanelTail: Bool = false) { - self.init( - context: context, - presentationData: presentationData, - configuration: ContextController.Configuration( - sources: [ContextController.Source( - id: AnyHashable(0 as Int), - title: "", - source: source, - items: items - )], - initialId: AnyHashable(0 as Int) - ), - recognizer: recognizer, - gesture: gesture, - workaroundUseLegacyImplementation: workaroundUseLegacyImplementation, - disableScreenshots: disableScreenshots, - hideReactionPanelTail: hideReactionPanelTail - ) - } +public final class ContextControllerSource { + public let id: AnyHashable + public let title: String + public let footer: String? + public let source: ContextContentSource + public let items: Signal + public let closeActionTitle: String? + public let closeAction: (() -> Void)? public init( - context: AccountContext? = nil, - presentationData: PresentationData, - configuration: ContextController.Configuration, - recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, - gesture: ContextGesture? = nil, - workaroundUseLegacyImplementation: Bool = false, - disableScreenshots: Bool = false, - hideReactionPanelTail: Bool = false + id: AnyHashable, + title: String, + footer: String? = nil, + source: ContextContentSource, + items: Signal, + closeActionTitle: String? = nil, + closeAction: (() -> Void)? = nil ) { - self.context = context - self.presentationData = presentationData - self.configuration = configuration - self.recognizer = recognizer - self.gesture = gesture - self.workaroundUseLegacyImplementation = workaroundUseLegacyImplementation - self.disableScreenshots = disableScreenshots - self.hideReactionPanelTail = hideReactionPanelTail - - super.init(navigationBarPresentationData: nil) - - if let mainSource = configuration.sources.first(where: { $0.id == configuration.initialId }) { - switch mainSource.source { - case let .location(locationSource): - self.statusBar.statusBarStyle = .Ignore - - self.shouldBeDismissedDisposable = (locationSource.shouldBeDismissed - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.dismiss(result: .default, completion: {}) - }).strict() - case let .reference(referenceSource): - self.statusBar.statusBarStyle = .Ignore - - self.shouldBeDismissedDisposable = (referenceSource.shouldBeDismissed - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.dismiss(result: .default, completion: {}) - }).strict() - case let .extracted(extractedSource): - if extractedSource.blurBackground { - self.statusBar.statusBarStyle = .Hide - } else { - self.statusBar.statusBarStyle = .Ignore - } - self.shouldBeDismissedDisposable = (extractedSource.shouldBeDismissed - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.dismiss(result: .default, completion: {}) - }).strict() - case .controller: - self.statusBar.statusBarStyle = .Hide - } - } - - self.lockOrientation = true - self.blocksBackgroundWhenInOverlay = true - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.shouldBeDismissedDisposable?.dispose() - } - - override public func loadDisplayNode() { - self.displayNode = ContextControllerNode(controller: self, context: self.context, presentationData: self.presentationData, configuration: self.configuration, beginDismiss: { [weak self] result in - self?.dismiss(result: result, completion: nil) - }, recognizer: self.recognizer, gesture: self.gesture, beganAnimatingOut: { [weak self] in - guard let strongSelf = self else { - return - } - - strongSelf.statusBar.statusBarStyle = .Ignore - - strongSelf.forEachController { c in - if let c = c as? UndoOverlayController { - c.dismiss() - } - return true - } - }, attemptTransitionControllerIntoNavigation: { - }) - self.controllerNode.dismissedForCancel = self.dismissedForCancel - self.displayNodeDidLoad() - - self._ready.set(combineLatest(queue: .mainQueue(), self.controllerNode.itemsReady.get(), self.controllerNode.contentReady.get()) - |> map { values in - return values.0 && values.1 - }) - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.controllerNode.updateLayout(layout: layout, transition: transition, previousActionsContainerNode: nil) - - if (layout.inputHeight ?? 0.0) == 0.0 { - if let dismissOnInputClose = self.dismissOnInputClose { - self.dismissOnInputClose = nil - DispatchQueue.main.async { - self.dismiss(result: dismissOnInputClose.result, completion: dismissOnInputClose.completion) - } - } else if let args = self.dismissToReactionOnInputClose { - self.dismissToReactionOnInputClose = nil - DispatchQueue.main.async { - self.dismissWithReactionImpl(value: args.value, targetView: args.targetView, hideNode: args.hideNode, animateTargetContainer: args.animateTargetContainer, addStandaloneReactionAnimation: args.addStandaloneReactionAnimation, reducedCurve: true, onHit: nil, completion: args.completion) - } - } - } - } - - override public func viewDidAppear(_ animated: Bool) { - if self.ignoreAppearanceMethodInvocations() { - return - } - super.viewDidAppear(animated) - - if !self.wasDismissed && !self.animatedDidAppear { - self.animatedDidAppear = true - self.controllerNode.animateIn() - } - } - - public func getActionsMinHeight() -> ContextController.ActionsHeight? { - if self.isNodeLoaded { - return self.controllerNode.getActionsMinHeight() - } - return nil - } - - public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, animated: Bool) { - //self.items = items - - if self.isNodeLoaded { - self.immediateItemsTransitionAnimation = false - self.controllerNode.setItemsSignal(items: items, minHeight: minHeight, previousActionsTransition: .scale, animated: animated) - } else { - assertionFailure() - } - } - - public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { - //self.items = items - - if self.isNodeLoaded { - self.controllerNode.setItemsSignal(items: items, minHeight: minHeight, previousActionsTransition: previousActionsTransition, animated: true) - } else { - assertionFailure() - } - } - - public func pushItems(items: Signal) { - if !self.isNodeLoaded { - return - } - self.controllerNode.pushItems(items: items) - } - - public func popItems() { - if !self.isNodeLoaded { - return - } - self.controllerNode.popItems() - } - - public func updateTheme(presentationData: PresentationData) { - self.presentationData = presentationData - if self.isNodeLoaded { - self.controllerNode.updateTheme(presentationData: presentationData) - } - } - - public func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) { - if let mainSource = self.configuration.sources.first(where: { $0.id == self.configuration.initialId }), case let .reference(source) = mainSource.source, source.forceDisplayBelowKeyboard { - - } else if viewTreeContainsFirstResponder(view: self.view) { - self.dismissOnInputClose = (result, completion) - self.view.endEditing(true) - return - } - - if !self.wasDismissed { - self.wasDismissed = true - - self.controllerNode.animateOut(result: result, completion: { [weak self] in - self?.presentingViewController?.dismiss(animated: false, completion: nil) - completion?() - }) - self.dismissed?() - } - } - - override public func dismiss(completion: (() -> Void)? = nil) { - self.dismiss(result: .default, completion: completion) - } - - public func dismissWithCustomTransition(transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) { - self.dismiss(result: .custom(transition), completion: nil) - } - - public func dismissWithoutContent() { - self.dismiss(result: .dismissWithoutContent, completion: nil) - } - - public func dismissNow() { - self.presentingViewController?.dismiss(animated: false, completion: nil) - self.dismissed?() - } - - public func dismissWithReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, onHit: (() -> Void)?, completion: (() -> Void)?) { - self.dismissWithReactionImpl(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: false, onHit: onHit, completion: completion) - } - - private func dismissWithReactionImpl(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: (() -> Void)?) { - if viewTreeContainsFirstResponder(view: self.view) { - self.dismissToReactionOnInputClose = (value, targetView, hideNode, animateTargetContainer, addStandaloneReactionAnimation, completion) - self.view.endEditing(true) - return - } - - if !self.wasDismissed { - self.wasDismissed = true - self.controllerNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, onHit: onHit, completion: { [weak self] in - self?.presentingViewController?.dismiss(animated: false, completion: nil) - completion?() - }) - self.dismissed?() - } - } - - public func animateDismissalIfNeeded() { - self.controllerNode.animateDismissalIfNeeded() - } - - public func cancelReactionAnimation() { - self.controllerNode.cancelReactionAnimation() - } - - public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { - self.controllerNode.addRelativeContentOffset(offset, transition: transition) - } - - public var keyShortcuts: [KeyShortcut] { - return [ - KeyShortcut( - input: UIKeyCommand.inputEscape, - modifiers: [], - action: { [weak self] in - self?.dismissWithoutContent() - } - ), - KeyShortcut( - input: "W", - modifiers: [.command], - action: { [weak self] in - self?.dismissWithoutContent() - } - ), - KeyShortcut( - input: "\r", - modifiers: [], - action: { [weak self] in - self?.controllerNode.performHighlightedAction() - } - ), - KeyShortcut( - input: UIKeyCommand.inputUpArrow, - modifiers: [], - action: { [weak self] in - self?.controllerNode.decreaseHighlightedIndex() - } - ), - KeyShortcut( - input: UIKeyCommand.inputDownArrow, - modifiers: [], - action: { [weak self] in - self?.controllerNode.increaseHighlightedIndex() - } - ) - ] + self.id = id + self.title = title + self.footer = footer + self.source = source + self.items = items + self.closeActionTitle = closeActionTitle + self.closeAction = closeAction } } + +public final class ContextControllerConfiguration { + public let sources: [ContextControllerSource] + public let initialId: AnyHashable + + public init(sources: [ContextControllerSource], initialId: AnyHashable) { + self.sources = sources + self.initialId = initialId + } +} + +public struct ContextControllerItems { + public enum Content { + case list([ContextMenuItem]) + case twoLists([ContextMenuItem], [ContextMenuItem]) + case custom(ContextControllerItemsContent) + } + + public var id: AnyHashable? + public var content: Content + public var context: AccountContext? + public var reactionItems: [ReactionContextItem] + public var selectedReactionItems: Set + public var reactionsTitle: String? + public var reactionsLocked: Bool + public var animationCache: AnimationCache? + public var alwaysAllowPremiumReactions: Bool + public var allPresetReactionsAreAvailable: Bool + public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? + public var disablePositionLock: Bool + public var previewReaction: TelegramMediaFile? + public var tip: ContextControllerTip? + public var tipSignal: Signal? + public var dismissed: (() -> Void)? + + public init( + id: AnyHashable? = nil, + content: Content, + context: AccountContext? = nil, + reactionItems: [ReactionContextItem] = [], + selectedReactionItems: Set = Set(), + reactionsTitle: String? = nil, + reactionsLocked: Bool = false, + animationCache: AnimationCache? = nil, + alwaysAllowPremiumReactions: Bool = false, + allPresetReactionsAreAvailable: Bool = false, + getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? = nil, + disablePositionLock: Bool = false, + previewReaction: TelegramMediaFile? = nil, + tip: ContextControllerTip? = nil, + tipSignal: Signal? = nil, + dismissed: (() -> Void)? = nil + ) { + self.id = id + self.content = content + self.context = context + self.animationCache = animationCache + self.reactionItems = reactionItems + self.selectedReactionItems = selectedReactionItems + self.reactionsTitle = reactionsTitle + self.reactionsLocked = reactionsLocked + self.alwaysAllowPremiumReactions = alwaysAllowPremiumReactions + self.allPresetReactionsAreAvailable = allPresetReactionsAreAvailable + self.getEmojiContent = getEmojiContent + self.disablePositionLock = disablePositionLock + self.previewReaction = previewReaction + self.tip = tip + self.tipSignal = tipSignal + self.dismissed = dismissed + } + + public init() { + self.id = nil + self.content = .list([]) + self.context = nil + self.reactionItems = [] + self.selectedReactionItems = Set() + self.reactionsTitle = nil + self.reactionsLocked = false + self.alwaysAllowPremiumReactions = false + self.allPresetReactionsAreAvailable = false + self.getEmojiContent = nil + self.disablePositionLock = false + self.previewReaction = nil + self.tip = nil + self.tipSignal = nil + self.dismissed = nil + } +} + +public enum ContextControllerPreviousActionsTransition { + case scale + case slide(forward: Bool) +} + +public enum ContextControllerTip: Equatable { + case textSelection + case quoteSelection + case messageViewsPrivacy + case messageCopyProtection(isChannel: Bool) + case animatedEmoji(text: String?, arguments: TextNodeWithEntities.Arguments?, file: TelegramMediaFile?, action: (() -> Void)?) + case notificationTopicExceptions(text: String, action: (() -> Void)?) + case starsReactions(topCount: Int) + case videoProcessing + case collageReordering + + public static func ==(lhs: ContextControllerTip, rhs: ContextControllerTip) -> Bool { + switch lhs { + case .textSelection: + if case .textSelection = rhs { + return true + } else { + return false + } + case .quoteSelection: + if case .quoteSelection = rhs { + return true + } else { + return false + } + case .messageViewsPrivacy: + if case .messageViewsPrivacy = rhs { + return true + } else { + return false + } + case let .messageCopyProtection(isChannel): + if case .messageCopyProtection(isChannel) = rhs { + return true + } else { + return false + } + case let .animatedEmoji(text, _, file, _): + if case let .animatedEmoji(rhsText, _, rhsFile, _) = rhs { + if text != rhsText { + return false + } + if file?.fileId != rhsFile?.fileId { + return false + } + return true + } else { + return false + } + case let .notificationTopicExceptions(text, _): + if case .notificationTopicExceptions(text, _) = rhs { + return true + } else { + return false + } + case let .starsReactions(topCount): + if case .starsReactions(topCount) = rhs { + return true + } else { + return false + } + case .videoProcessing: + if case .videoProcessing = rhs { + return true + } else { + return false + } + case .collageReordering: + if case .collageReordering = rhs { + return true + } else { + return false + } + } + } +} + +public final class ContextControllerActionsHeight { + public let minY: CGFloat + public let contentOffset: CGFloat + + public init(minY: CGFloat, contentOffset: CGFloat) { + self.minY = minY + self.contentOffset = contentOffset + } +} + +public enum ContextControllerHandledTouchEvent { + case ignore + case dismiss(consume: Bool, result: UIView?) +} + +public enum ContextActionSibling { + case none + case item + case separator +} + +public protocol ContextActionNodeProtocol: ASDisplayNode { + func setIsHighlighted(_ value: Bool) + func performAction() + func actionNode(at point: CGPoint) -> ContextActionNodeProtocol + var isActionEnabled: Bool { get } +} + +public protocol ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol, KeyShortcutResponder { + typealias ContentSource = ContextContentSource + typealias ItemsNode = ContextControllerItemsNode + typealias ItemsContent = ContextControllerItemsContent + typealias Source = ContextControllerSource + typealias Configuration = ContextControllerConfiguration + typealias Items = ContextControllerItems + typealias PreviousActionsTransition = ContextControllerPreviousActionsTransition + typealias Tip = ContextControllerTip + typealias ActionsHeight = ContextControllerActionsHeight + typealias HandledTouchEvent = ContextControllerHandledTouchEvent + + var dismissed: (() -> Void)? { get set } + var dismissedForCancel: (() -> Void)? { get set } + var passthroughTouchEvent: ((UIView, CGPoint) -> HandledTouchEvent)? { get set } + var reactionSelected: ((UpdateMessageReaction, Bool) -> Void)? { get set } + var premiumReactionsSelected: (() -> Void)? { get set } + var getOverlayViews: (() -> [UIView])? { get set } + + func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) + func updateTheme(presentationData: PresentationData) + func dismissWithCustomTransition(transition: ContainedViewLayoutTransition, completion: (() -> Void)?) + func dismissWithoutContent() + func dismissNow() + func dismissWithReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, onHit: (() -> Void)?, completion: (() -> Void)?) + func animateDismissalIfNeeded() + func cancelReactionAnimation() +} + +public func makeContextController( + context: AccountContext? = nil, + presentationData: PresentationData, + source: ContextContentSource, + items: Signal, + recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, + gesture: ContextGesture? = nil, + workaroundUseLegacyImplementation: Bool = false, + disableScreenshots: Bool = false, + hideReactionPanelTail: Bool = false +) -> ContextController { + return makeContextController( + context: context, + presentationData: presentationData, + configuration: ContextController.Configuration( + sources: [ContextController.Source( + id: AnyHashable(0 as Int), + title: "", + source: source, + items: items + )], + initialId: AnyHashable(0 as Int) + ), + recognizer: recognizer, + gesture: gesture, + workaroundUseLegacyImplementation: workaroundUseLegacyImplementation, + disableScreenshots: disableScreenshots, + hideReactionPanelTail: hideReactionPanelTail + ) +} + +public var makeContextControllerImpl: (( + _ context: AccountContext?, + _ presentationData: PresentationData, + _ configuration: ContextController.Configuration, + _ recognizer: TapLongTapOrDoubleTapGestureRecognizer?, + _ gesture: ContextGesture?, + _ workaroundUseLegacyImplementation: Bool, + _ disableScreenshots: Bool, + _ hideReactionPanelTail: Bool +) -> ContextController)? = nil + +public func makeContextController( + context: AccountContext? = nil, + presentationData: PresentationData, + configuration: ContextController.Configuration, + recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, + gesture: ContextGesture? = nil, + workaroundUseLegacyImplementation: Bool = false, + disableScreenshots: Bool = false, + hideReactionPanelTail: Bool = false +) -> ContextController { + return makeContextControllerImpl!( + context, + presentationData, + configuration, + recognizer, + gesture, + workaroundUseLegacyImplementation, + disableScreenshots, + hideReactionPanelTail + ) +} + +public enum ContextControllerActionsStackNodePresentation { + case modal + case inline + case additional +} + +public protocol ContextControllerActionsStackItemNode: ASDisplayNode { + var wantsFullWidth: Bool { get } + + func update( + presentationData: PresentationData, + constrainedSize: CGSize, + standardMinWidth: CGFloat, + standardMaxWidth: CGFloat, + additionalBottomInset: CGFloat, + transition: ContainedViewLayoutTransition + ) -> (size: CGSize, apparentHeight: CGFloat) + + func highlightGestureMoved(location: CGPoint) + func highlightGestureFinished(performAction: Bool) + + func decreaseHighlightedIndex() + func increaseHighlightedIndex() +} + +public protocol ContextControllerActionsStackItem: AnyObject { + func node( + context: AccountContext?, + getController: @escaping () -> ContextControllerProtocol?, + requestDismiss: @escaping (ContextMenuActionResult) -> Void, + requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, + requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void + ) -> ContextControllerActionsStackItemNode + + var id: AnyHashable? { get } + var tip: ContextController.Tip? { get } + var tipSignal: Signal? { get } + var reactionItems: ContextControllerReactionItems? { get } + var previewReaction: ContextControllerPreviewReaction? { get } + var dismissed: (() -> Void)? { get } +} + +public struct ContextControllerReactionItems { + public var context: AccountContext + public var reactionItems: [ReactionContextItem] + public var selectedReactionItems: Set + public var reactionsTitle: String? + public var reactionsLocked: Bool + public var animationCache: AnimationCache + public var alwaysAllowPremiumReactions: Bool + public var allPresetReactionsAreAvailable: Bool + public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? + + public init(context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, reactionsTitle: String?, reactionsLocked: Bool, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, allPresetReactionsAreAvailable: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?) { + self.context = context + self.reactionItems = reactionItems + self.selectedReactionItems = selectedReactionItems + self.reactionsTitle = reactionsTitle + self.reactionsLocked = reactionsLocked + self.animationCache = animationCache + self.alwaysAllowPremiumReactions = alwaysAllowPremiumReactions + self.allPresetReactionsAreAvailable = allPresetReactionsAreAvailable + self.getEmojiContent = getEmojiContent + } +} + +public final class ContextControllerPreviewReaction { + public let context: AccountContext + public let file: TelegramMediaFile + + public init(context: AccountContext, file: TelegramMediaFile) { + self.context = context + self.file = file + } +} + +public protocol ContextControllerActionsStackNode: ASDisplayNode { + typealias Presentation = ContextControllerActionsStackNodePresentation + + var topReactionItems: ContextControllerReactionItems? { get } + var topPreviewReaction: ContextControllerPreviewReaction? { get } + var topPositionLock: CGFloat? { get } + var storedScrollingState: CGFloat? { get } + + func replace(item: ContextControllerActionsStackItem, animated: Bool?) + func push(item: ContextControllerActionsStackItem, currentScrollingState: CGFloat?, positionLock: CGFloat?, animated: Bool) + func clearStoredScrollingState() + func pop() + func update( + presentationData: PresentationData, + constrainedSize: CGSize, + presentation: Presentation, + transition: ContainedViewLayoutTransition + ) -> CGSize + func highlightGestureMoved(location: CGPoint) + func highlightGestureFinished(performAction: Bool) + func decreaseHighlightedIndex() + func increaseHighlightedIndex() + func updatePanSelection(isEnabled: Bool) + func animateIn() +} + +public var makeContextControllerActionsListStackItemImpl: (( + _ id: AnyHashable?, + _ items: [ContextMenuItem], + _ reactionItems: ContextControllerReactionItems?, + _ previewReaction: ContextControllerPreviewReaction?, + _ tip: ContextController.Tip?, + _ tipSignal: Signal?, + _ dismissed: (() -> Void)? +) -> ContextControllerActionsStackItem)? + +public func makeContextControllerActionsListStackItem( + id: AnyHashable?, + items: [ContextMenuItem], + reactionItems: ContextControllerReactionItems?, + previewReaction: ContextControllerPreviewReaction?, + tip: ContextController.Tip?, + tipSignal: Signal?, + dismissed: (() -> Void)? +) -> ContextControllerActionsStackItem { + return makeContextControllerActionsListStackItemImpl!( + id, + items, + reactionItems, + previewReaction, + tip, + tipSignal, + dismissed + ) +} + +public var makeContextControllerActionsStackNodeImpl: (( + _ context: AccountContext?, + _ getController: @escaping () -> ContextControllerProtocol?, + _ requestDismiss: @escaping (ContextMenuActionResult) -> Void, + _ requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void +) -> ContextControllerActionsStackNode)? + +public func makeContextControllerActionsStackNode( + context: AccountContext?, + getController: @escaping () -> ContextControllerProtocol?, + requestDismiss: @escaping (ContextMenuActionResult) -> Void, + requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void +) -> ContextControllerActionsStackNode { + return makeContextControllerActionsStackNodeImpl!( + context, + getController, + requestDismiss, + requestUpdate + ) +} + +public var makeContextActionNodeImpl: (( + _ presentationData: PresentationData, + _ action: ContextMenuActionItem, + _ getController: @escaping () -> ContextControllerProtocol?, + _ actionSelected: @escaping (ContextMenuActionResult) -> Void, + _ requestLayout: @escaping () -> Void, + _ requestUpdateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void +) -> ContextActionNodeProtocol)? + +public func makeContextActionNode( + presentationData: PresentationData, + action: ContextMenuActionItem, + getController: @escaping () -> ContextControllerProtocol?, + actionSelected: @escaping (ContextMenuActionResult) -> Void, + requestLayout: @escaping () -> Void, + requestUpdateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void +) -> ContextActionNodeProtocol { + return makeContextActionNodeImpl!( + presentationData, + action, + getController, + actionSelected, + requestLayout, + requestUpdateAction + ) +} diff --git a/submodules/ContextUI/Sources/PeekController.swift b/submodules/ContextUI/Sources/PeekController.swift index 74af9c181d..fafbe42f39 100644 --- a/submodules/ContextUI/Sources/PeekController.swift +++ b/submodules/ContextUI/Sources/PeekController.swift @@ -5,6 +5,39 @@ import Display import SwiftSignalKit import TelegramPresentationData +public enum PeekControllerContentPresentation { + case contained + case freeform +} + +public enum PeerControllerMenuActivation { + case drag + case press +} + +public protocol PeekControllerContent { + func presentation() -> PeekControllerContentPresentation + func menuActivation() -> PeerControllerMenuActivation + func menuItems() -> [ContextMenuItem] + func node() -> PeekControllerContentNode & ASDisplayNode + + func topAccessoryNode() -> ASDisplayNode? + func fullScreenAccessoryNode(blurView: UIVisualEffectView) -> (PeekControllerAccessoryNode & ASDisplayNode)? + + func isEqual(to: PeekControllerContent) -> Bool +} + +public protocol PeekControllerContentNode { + func ready() -> Signal + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize +} + +public protocol PeekControllerAccessoryNode { + var dismiss: () -> Void { get set } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) +} + public final class PeekControllerTheme { public let isDark: Bool public let menuBackgroundColor: UIColor @@ -30,115 +63,32 @@ extension PeekControllerTheme { } } -public final class PeekController: ViewController, ContextControllerProtocol { - public var useComplexItemsTransitionAnimation: Bool = false - public var immediateItemsTransitionAnimation = false - - public func getActionsMinHeight() -> ContextController.ActionsHeight? { - return nil - } - - public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, animated: Bool) { - } - - public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { - } - - public func pushItems(items: Signal) { - self.controllerNode.pushItems(items: items) - } - - public func popItems() { - self.controllerNode.popItems() - } - - private var controllerNode: PeekControllerNode { - return self.displayNode as! PeekControllerNode - } - - public var contentNode: PeekControllerContentNode & ASDisplayNode { - return self.controllerNode.contentNode - } - - private let presentationData: PresentationData - private let content: PeekControllerContent - var sourceView: () -> (UIView, CGRect)? - private let activateImmediately: Bool - - public var visibilityUpdated: ((Bool) -> Void)? - - public var getOverlayViews: (() -> [UIView])? - - public var appeared: (() -> Void)? - public var disappeared: (() -> Void)? - - private var animatedIn = false - - private let _ready = Promise() - override public var ready: Promise { - return self._ready - } - - public init(presentationData: PresentationData, content: PeekControllerContent, sourceView: @escaping () -> (UIView, CGRect)?, activateImmediately: Bool = false) { - self.presentationData = presentationData - self.content = content - self.sourceView = sourceView - self.activateImmediately = activateImmediately - - super.init(navigationBarPresentationData: nil) - - self.statusBar.statusBarStyle = .Ignore - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func loadDisplayNode() { - self.displayNode = PeekControllerNode(presentationData: self.presentationData, controller: self, content: self.content, requestDismiss: { [weak self] in - self?.dismiss() - }) - self.displayNodeDidLoad() - } - - private func getSourceRect() -> CGRect { - if let (sourceView, sourceRect) = self.sourceView() { - return sourceView.convert(sourceRect, to: self.view) - } else { - let size = self.displayNode.bounds.size - return CGRect(origin: CGPoint(x: floor((size.width - 10.0) / 2.0), y: floor((size.height - 10.0) / 2.0)), size: CGSize(width: 10.0, height: 10.0)) - } - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if !self.animatedIn { - self.animatedIn = true - self.controllerNode.animateIn(from: self.getSourceRect()) - - self.visibilityUpdated?(true) - - if self.activateImmediately { - self.controllerNode.activateMenu(immediately: true) - } - } - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.controllerNode.containerLayoutUpdated(layout, transition: transition) - } - - override public func dismiss(completion: (() -> Void)? = nil) { - self.visibilityUpdated?(false) - self.controllerNode.animateOut(to: self.getSourceRect(), completion: { [weak self] in - self?.presentingViewController?.dismiss(animated: false, completion: nil) - }) - } - - public func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) { - self.dismiss(completion: completion) - } +public protocol PeekController: ViewController, ContextControllerProtocol { + var visibilityUpdated: ((Bool) -> Void)? { get set } + var getOverlayViews: (() -> [UIView])? { get set } + var appeared: (() -> Void)? { get set } + var disappeared: (() -> Void)? { get set } + var sourceView: () -> (UIView, CGRect)? { get set } + var contentNode: PeekControllerContentNode & ASDisplayNode { get } +} + +public var makePeekControllerImpl: (( + _ presentationData: PresentationData, + _ content: PeekControllerContent, + _ sourceView: @escaping () -> (UIView, CGRect)?, + _ activateImmediately: Bool +) -> PeekController)? + +public func makePeekController( + presentationData: PresentationData, + content: PeekControllerContent, + sourceView: @escaping () -> (UIView, CGRect)?, + activateImmediately: Bool = false +) -> PeekController { + return makePeekControllerImpl!( + presentationData, + content, + sourceView, + activateImmediately + ) } diff --git a/submodules/ContextUI/Sources/PeekControllerContent.swift b/submodules/ContextUI/Sources/PeekControllerContent.swift deleted file mode 100644 index f5b877fc12..0000000000 --- a/submodules/ContextUI/Sources/PeekControllerContent.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import SwiftSignalKit - -public enum PeekControllerContentPresentation { - case contained - case freeform -} - -public enum PeerControllerMenuActivation { - case drag - case press -} - -public protocol PeekControllerContent { - func presentation() -> PeekControllerContentPresentation - func menuActivation() -> PeerControllerMenuActivation - func menuItems() -> [ContextMenuItem] - func node() -> PeekControllerContentNode & ASDisplayNode - - func topAccessoryNode() -> ASDisplayNode? - func fullScreenAccessoryNode(blurView: UIVisualEffectView) -> (PeekControllerAccessoryNode & ASDisplayNode)? - - func isEqual(to: PeekControllerContent) -> Bool -} - -public protocol PeekControllerContentNode { - func ready() -> Signal - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize -} - -public protocol PeekControllerAccessoryNode { - var dismiss: () -> Void { get set } - - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -} diff --git a/submodules/ContextUI/Sources/PeekControllerGestureRecognizer.swift b/submodules/ContextUI/Sources/PeekControllerGestureRecognizer.swift index 26e1025f13..3d1d014371 100644 --- a/submodules/ContextUI/Sources/PeekControllerGestureRecognizer.swift +++ b/submodules/ContextUI/Sources/PeekControllerGestureRecognizer.swift @@ -4,6 +4,13 @@ import SwiftSignalKit import AsyncDisplayKit import Display +public protocol PeekControllerNodeProtocol: AnyObject { + func applyDraggingOffset(_ offset: CGPoint) + func activateMenu(immediately: Bool) + func endDragging(_ location: CGPoint) + func updateContent(content: PeekControllerContent) +} + private func traceDeceleratingScrollView(_ view: UIView, at point: CGPoint) -> Bool { if view.bounds.contains(point), let view = view as? UIScrollView, view.isDecelerating { return true @@ -108,7 +115,7 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { if let _ = self.tapLocation, let menuActivation = self.menuActivation, case .press = menuActivation { if let presentedController = self.presentedController { if presentedController.isNodeLoaded { - (presentedController.displayNode as? PeekControllerNode)?.activateMenu() + (presentedController.displayNode as? PeekControllerNodeProtocol)?.activateMenu(immediately: false) } self.menuActivation = nil // self.presentedController = nil @@ -148,7 +155,7 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { self.state = .ended } else { if let presentedController = self.presentedController, presentedController.isNodeLoaded, let location = touches.first?.location(in: presentedController.view) { - (presentedController.displayNode as? PeekControllerNode)?.endDragging(location) + (presentedController.displayNode as? PeekControllerNodeProtocol)?.endDragging(location) self.presentedController = nil self.menuActivation = nil } @@ -184,7 +191,7 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { if let presentedController = self.presentedController, self.menuActivation == nil { if presentedController.isNodeLoaded { let touchLocation = touch.location(in: presentedController.view) - (presentedController.displayNode as? PeekControllerNode)?.applyDraggingOffset(touchLocation) + (presentedController.displayNode as? PeekControllerNodeProtocol)?.applyDraggingOffset(touchLocation) } } else if let menuActivation = self.menuActivation, let presentedController = self.presentedController { switch menuActivation { @@ -201,7 +208,7 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { if touch.force >= 2.5 { if presentedController.isNodeLoaded { - (presentedController.displayNode as? PeekControllerNode)?.activateMenu() + (presentedController.displayNode as? PeekControllerNodeProtocol)?.activateMenu(immediately: false) self.menuActivation = nil self.presentedController = nil self.candidateContent = nil @@ -268,7 +275,7 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { presentedController.sourceView = { return (sourceView, sourceRect) } - (presentedController.displayNode as? PeekControllerNode)?.updateContent(content: content) + (presentedController.displayNode as? PeekControllerNodeProtocol)?.updateContent(content: content) } } } else { @@ -277,7 +284,7 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { if forceActivate { strongSelf.candidateContent = nil if case .press = content.menuActivation() { - (presentedController.displayNode as? PeekControllerNode)?.activateMenu() + (presentedController.displayNode as? PeekControllerNodeProtocol)?.activateMenu(immediately: false) } } else { strongSelf.candidateContent = (sourceView, sourceRect, content) diff --git a/submodules/ContextUI/Sources/PinchController.swift b/submodules/ContextUI/Sources/PinchController.swift index 17082739c1..0783bd7a22 100644 --- a/submodules/ContextUI/Sources/PinchController.swift +++ b/submodules/ContextUI/Sources/PinchController.swift @@ -3,480 +3,25 @@ import UIKit import AsyncDisplayKit import Display import TelegramPresentationData -import TextSelectionNode -import TelegramCore -import SwiftSignalKit -import UIKitRuntimeUtils -final class PinchSourceGesture: UIPinchGestureRecognizer { - private final class Target { - var updated: (() -> Void)? - - @objc func onGesture(_ gesture: UIPinchGestureRecognizer) { - self.updated?() - } - } - - private let target: Target - - private(set) var currentTransform: (CGFloat, CGPoint, CGPoint)? - - var began: (() -> Void)? - var updated: ((CGFloat, CGPoint, CGPoint) -> Void)? - var ended: (() -> Void)? - - private var initialLocation: CGPoint? - private var pinchLocation = CGPoint() - private var currentOffset = CGPoint() - - private var currentNumberOfTouches = 0 - - init() { - self.target = Target() - - super.init(target: self.target, action: #selector(self.target.onGesture(_:))) - - self.target.updated = { [weak self] in - self?.gestureUpdated() - } - } - - override func reset() { - super.reset() - - self.currentNumberOfTouches = 0 - self.initialLocation = nil - } - - override func touchesBegan(_ touches: Set, with event: UIEvent) { - super.touchesBegan(touches, with: event) - - //self.currentTouches.formUnion(touches) - } - - override func touchesEnded(_ touches: Set, with event: UIEvent) { - super.touchesEnded(touches, with: event) - } - - override func touchesCancelled(_ touches: Set, with event: UIEvent) { - super.touchesCancelled(touches, with: event) - } - - override func touchesMoved(_ touches: Set, with event: UIEvent) { - super.touchesMoved(touches, with: event) - } - - private func gestureUpdated() { - switch self.state { - case .began: - self.currentOffset = CGPoint() - - let pinchLocation = self.location(in: self.view) - self.pinchLocation = pinchLocation - self.initialLocation = pinchLocation - let scale = max(1.0, self.scale) - self.currentTransform = (scale, self.pinchLocation, self.currentOffset) - - self.currentNumberOfTouches = self.numberOfTouches - - self.began?() - case .changed: - let locationSum = self.location(in: self.view) - - if self.numberOfTouches < 2 && self.currentNumberOfTouches >= 2 { - self.initialLocation = CGPoint(x: locationSum.x - self.currentOffset.x, y: locationSum.y - self.currentOffset.y) - } - self.currentNumberOfTouches = self.numberOfTouches - - if let initialLocation = self.initialLocation { - self.currentOffset = CGPoint(x: locationSum.x - initialLocation.x, y: locationSum.y - initialLocation.y) - } - if let (scale, pinchLocation, _) = self.currentTransform { - self.currentTransform = (scale, pinchLocation, self.currentOffset) - self.updated?(scale, pinchLocation, self.currentOffset) - } - - let scale = max(1.0, self.scale) - self.currentTransform = (scale, self.pinchLocation, self.currentOffset) - self.updated?(scale, self.pinchLocation, self.currentOffset) - case .ended, .cancelled: - self.ended?() - default: - break - } - } +public protocol PinchController: ViewController { + func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) } -private func cancelContextGestures(node: ASDisplayNode) { - if let node = node as? ContextControllerSourceNode { - node.cancelGesture() - } +public var makePinchControllerImpl: (( + _ sourceNode: PinchSourceContainerNode, + _ disableScreenshots: Bool, + _ getContentAreaInScreenSpace: @escaping () -> CGRect +) -> PinchController)? - if let supernode = node.supernode { - cancelContextGestures(node: supernode) - } -} - -private func cancelContextGestures(view: UIView) { - if let gestureRecognizers = view.gestureRecognizers { - for recognizer in gestureRecognizers { - if let recognizer = recognizer as? InteractiveTransitionGestureRecognizer { - recognizer.cancel() - } else if let recognizer = recognizer as? WindowPanRecognizer { - recognizer.cancel() - } - } - } - - if let superview = view.superview { - cancelContextGestures(view: superview) - } -} - -public final class PinchSourceContainerNode: ASDisplayNode, ASGestureRecognizerDelegate { - public let contentNode: ASDisplayNode - public var contentRect: CGRect = CGRect() - private(set) var naturalContentFrame: CGRect? - - fileprivate let gesture: PinchSourceGesture - - public var isPinchGestureEnabled: Bool = true { - didSet { - if self.isPinchGestureEnabled != oldValue { - self.gesture.isEnabled = self.isPinchGestureEnabled - } - } - } - - public var maxPinchScale: CGFloat = 10.0 - - private var isActive: Bool = false - - public var activate: ((PinchSourceContainerNode) -> Void)? - public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? - public var animatedOut: (() -> Void)? - var deactivate: (() -> Void)? - public var deactivated: (() -> Void)? - var updated: ((CGFloat, CGPoint, CGPoint) -> Void)? - - override public init() { - self.gesture = PinchSourceGesture() - self.contentNode = ASDisplayNode() - - super.init() - - self.addSubnode(self.contentNode) - - self.gesture.began = { [weak self] in - guard let strongSelf = self else { - return - } - cancelContextGestures(node: strongSelf) - cancelContextGestures(view: strongSelf.view) - strongSelf.isActive = true - - strongSelf.activate?(strongSelf) - } - - self.gesture.ended = { [weak self] in - guard let strongSelf = self else { - return - } - - strongSelf.isActive = false - strongSelf.deactivate?() - strongSelf.deactivated?() - } - - self.gesture.updated = { [weak self] scale, pinchLocation, offset in - guard let strongSelf = self else { - return - } - strongSelf.updated?(min(scale, strongSelf.maxPinchScale), pinchLocation, offset) - strongSelf.scaleUpdated?(min(scale, strongSelf.maxPinchScale), .immediate) - } - } - - override public func didLoad() { - super.didLoad() - - self.view.addGestureRecognizer(self.gesture) - self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in - guard let strongSelf = self else { - return false - } - return strongSelf.isActive - } - } - - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return false - } - - public func update(size: CGSize, transition: ContainedViewLayoutTransition) { - let contentFrame = CGRect(origin: CGPoint(), size: size) - self.naturalContentFrame = contentFrame - if !self.isActive { - transition.updateFrame(node: self.contentNode, frame: contentFrame) - } - } - - func restoreToNaturalSize() { - guard let naturalContentFrame = self.naturalContentFrame else { - return - } - self.contentNode.frame = naturalContentFrame - } -} - -private final class PinchControllerNode: ViewControllerTracingNode { - private weak var controller: PinchController? - - private var initialSourceFrame: CGRect? - - private let clippingNode: ASDisplayNode - private let scrollingContainer: ASDisplayNode - - private let sourceNode: PinchSourceContainerNode - private let disableScreenshots: Bool - private let getContentAreaInScreenSpace: () -> CGRect - - private let dimNode: ASDisplayNode - - private var validLayout: ContainerViewLayout? - private var isAnimatingOut: Bool = false - - private var hapticFeedback: HapticFeedback? - - init(controller: PinchController, sourceNode: PinchSourceContainerNode, disableScreenshots: Bool, getContentAreaInScreenSpace: @escaping () -> CGRect) { - self.controller = controller - self.sourceNode = sourceNode - self.disableScreenshots = disableScreenshots - self.getContentAreaInScreenSpace = getContentAreaInScreenSpace - - self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - self.dimNode.alpha = 0.0 - - self.clippingNode = ASDisplayNode() - self.clippingNode.clipsToBounds = true - - self.scrollingContainer = ASDisplayNode() - - super.init() - - self.addSubnode(self.dimNode) - self.addSubnode(self.clippingNode) - self.clippingNode.addSubnode(self.scrollingContainer) - - self.sourceNode.deactivate = { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.controller?.dismiss() - } - - self.sourceNode.updated = { [weak self] scale, pinchLocation, offset in - guard let strongSelf = self, let initialSourceFrame = strongSelf.initialSourceFrame else { - return - } - strongSelf.dimNode.alpha = max(0.0, min(1.0, scale - 1.0)) - - let pinchOffset = CGPoint( - x: pinchLocation.x - initialSourceFrame.width / 2.0, - y: pinchLocation.y - initialSourceFrame.height / 2.0 - ) - - var transform = CATransform3DIdentity - transform = CATransform3DTranslate(transform, offset.x - pinchOffset.x * (scale - 1.0), offset.y - pinchOffset.y * (scale - 1.0), 0.0) - transform = CATransform3DScale(transform, scale, scale, 0.0) - - strongSelf.sourceNode.contentNode.transform = transform - } - - if self.disableScreenshots { - setLayerDisableScreenshots(self.layer, true) - } - } - - deinit { - } - - override func didLoad() { - super.didLoad() - } - - func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?) { - if self.isAnimatingOut { - return - } - - self.validLayout = layout - - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - } - - func animateIn() { - let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view) - self.sourceNode.contentNode.frame = convertedFrame - self.initialSourceFrame = convertedFrame - self.scrollingContainer.addSubnode(self.sourceNode.contentNode) - - var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace() - updatedContentAreaInScreenSpace.origin.x = 0.0 - updatedContentAreaInScreenSpace.size.width = self.bounds.width - - self.clippingNode.layer.animateFrame(from: updatedContentAreaInScreenSpace, to: self.clippingNode.frame, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) - self.clippingNode.layer.animateBoundsOriginYAdditive(from: updatedContentAreaInScreenSpace.minY, to: 0.0, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) - } - - func animateOut(completion: @escaping () -> Void) { - self.isAnimatingOut = true - - let performCompletion: () -> Void = { [weak self] in - guard let strongSelf = self else { - return - } - - strongSelf.isAnimatingOut = false - - strongSelf.sourceNode.restoreToNaturalSize() - strongSelf.sourceNode.addSubnode(strongSelf.sourceNode.contentNode) - - strongSelf.sourceNode.animatedOut?() - - completion() - } - - let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view) - self.sourceNode.contentNode.frame = convertedFrame - self.initialSourceFrame = convertedFrame - - if let (scale, pinchLocation, offset) = self.sourceNode.gesture.currentTransform, let initialSourceFrame = self.initialSourceFrame { - let duration = 0.3 - let transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut - - var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace() - updatedContentAreaInScreenSpace.origin.x = 0.0 - updatedContentAreaInScreenSpace.size.width = self.bounds.width - - self.clippingNode.layer.animateFrame(from: self.clippingNode.frame, to: updatedContentAreaInScreenSpace, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) - self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) - - let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: .spring) - if self.hapticFeedback == nil { - self.hapticFeedback = HapticFeedback() - } - self.hapticFeedback?.prepareImpact(.light) - self.hapticFeedback?.impact(.light) - - self.sourceNode.scaleUpdated?(1.0, transition) - - let pinchOffset = CGPoint( - x: pinchLocation.x - initialSourceFrame.width / 2.0, - y: pinchLocation.y - initialSourceFrame.height / 2.0 - ) - - var transform = CATransform3DIdentity - transform = CATransform3DScale(transform, scale, scale, 0.0) - - self.sourceNode.contentNode.transform = CATransform3DIdentity - self.sourceNode.contentNode.position = CGPoint(x: initialSourceFrame.midX, y: initialSourceFrame.midY) - self.sourceNode.contentNode.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration * 1.2, damping: 110.0) - self.sourceNode.contentNode.layer.animatePosition(from: CGPoint(x: offset.x - pinchOffset.x * (scale - 1.0), y: offset.y - pinchOffset.y * (scale - 1.0)), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { _ in - performCompletion() - }) - - let dimNodeTransition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: transitionCurve) - dimNodeTransition.updateAlpha(node: self.dimNode, alpha: 0.0) - } else { - performCompletion() - } - } - - func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { - if self.isAnimatingOut { - self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset.y) - transition.animateOffsetAdditive(node: self.scrollingContainer, offset: -offset.y) - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - return nil - } -} - -public final class PinchController: ViewController, StandalonePresentableController { - private let _ready = Promise() - override public var ready: Promise { - return self._ready - } - - private let sourceNode: PinchSourceContainerNode - private let disableScreenshots: Bool - private let getContentAreaInScreenSpace: () -> CGRect - - private var wasDismissed = false - - private var controllerNode: PinchControllerNode { - return self.displayNode as! PinchControllerNode - } - - public init(sourceNode: PinchSourceContainerNode, disableScreenshots: Bool = false, getContentAreaInScreenSpace: @escaping () -> CGRect) { - self.sourceNode = sourceNode - self.disableScreenshots = disableScreenshots - self.getContentAreaInScreenSpace = getContentAreaInScreenSpace - - super.init(navigationBarPresentationData: nil) - - self.statusBar.statusBarStyle = .Ignore - - self.lockOrientation = true - self.blocksBackgroundWhenInOverlay = true - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - } - - override public func loadDisplayNode() { - self.displayNode = PinchControllerNode(controller: self, sourceNode: self.sourceNode, disableScreenshots: self.disableScreenshots, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) - - self.displayNodeDidLoad() - - self._ready.set(.single(true)) - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.controllerNode.updateLayout(layout: layout, transition: transition, previousActionsContainerNode: nil) - } - - override public func viewDidAppear(_ animated: Bool) { - if self.ignoreAppearanceMethodInvocations() { - return - } - super.viewDidAppear(animated) - - self.controllerNode.animateIn() - } - - override public func dismiss(completion: (() -> Void)? = nil) { - if !self.wasDismissed { - self.wasDismissed = true - self.controllerNode.animateOut(completion: { [weak self] in - self?.presentingViewController?.dismiss(animated: false, completion: nil) - completion?() - }) - } - } - - public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { - self.controllerNode.addRelativeContentOffset(offset, transition: transition) - } +public func makePinchController( + sourceNode: PinchSourceContainerNode, + disableScreenshots: Bool = false, + getContentAreaInScreenSpace: @escaping () -> CGRect +) -> PinchController { + return makePinchControllerImpl!( + sourceNode, + disableScreenshots, + getContentAreaInScreenSpace + ) } diff --git a/submodules/ContextUI/Sources/PinchSourceContainerNode.swift b/submodules/ContextUI/Sources/PinchSourceContainerNode.swift new file mode 100644 index 0000000000..bc2cc63ac6 --- /dev/null +++ b/submodules/ContextUI/Sources/PinchSourceContainerNode.swift @@ -0,0 +1,226 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display + +private func cancelContextGestures(node: ASDisplayNode) { + if let node = node as? ContextControllerSourceNode { + node.cancelGesture() + } + + if let supernode = node.supernode { + cancelContextGestures(node: supernode) + } +} + +private func cancelContextGestures(view: UIView) { + if let gestureRecognizers = view.gestureRecognizers { + for recognizer in gestureRecognizers { + if let recognizer = recognizer as? InteractiveTransitionGestureRecognizer { + recognizer.cancel() + } else if let recognizer = recognizer as? WindowPanRecognizer { + recognizer.cancel() + } + } + } + + if let superview = view.superview { + cancelContextGestures(view: superview) + } +} + +public final class PinchSourceGesture: UIPinchGestureRecognizer { + private final class Target { + var updated: (() -> Void)? + + @objc func onGesture(_ gesture: UIPinchGestureRecognizer) { + self.updated?() + } + } + + private let target: Target + + public private(set) var currentTransform: (CGFloat, CGPoint, CGPoint)? + + public var began: (() -> Void)? + public var updated: ((CGFloat, CGPoint, CGPoint) -> Void)? + public var ended: (() -> Void)? + + private var initialLocation: CGPoint? + private var pinchLocation = CGPoint() + private var currentOffset = CGPoint() + + private var currentNumberOfTouches = 0 + + public init() { + self.target = Target() + + super.init(target: self.target, action: #selector(self.target.onGesture(_:))) + + self.target.updated = { [weak self] in + self?.gestureUpdated() + } + } + + override public func reset() { + super.reset() + + self.currentNumberOfTouches = 0 + self.initialLocation = nil + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + //self.currentTouches.formUnion(touches) + } + + override public func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + } + + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + } + + private func gestureUpdated() { + switch self.state { + case .began: + self.currentOffset = CGPoint() + + let pinchLocation = self.location(in: self.view) + self.pinchLocation = pinchLocation + self.initialLocation = pinchLocation + let scale = max(1.0, self.scale) + self.currentTransform = (scale, self.pinchLocation, self.currentOffset) + + self.currentNumberOfTouches = self.numberOfTouches + + self.began?() + case .changed: + let locationSum = self.location(in: self.view) + + if self.numberOfTouches < 2 && self.currentNumberOfTouches >= 2 { + self.initialLocation = CGPoint(x: locationSum.x - self.currentOffset.x, y: locationSum.y - self.currentOffset.y) + } + self.currentNumberOfTouches = self.numberOfTouches + + if let initialLocation = self.initialLocation { + self.currentOffset = CGPoint(x: locationSum.x - initialLocation.x, y: locationSum.y - initialLocation.y) + } + if let (scale, pinchLocation, _) = self.currentTransform { + self.currentTransform = (scale, pinchLocation, self.currentOffset) + self.updated?(scale, pinchLocation, self.currentOffset) + } + + let scale = max(1.0, self.scale) + self.currentTransform = (scale, self.pinchLocation, self.currentOffset) + self.updated?(scale, self.pinchLocation, self.currentOffset) + case .ended, .cancelled: + self.ended?() + default: + break + } + } +} + + +public final class PinchSourceContainerNode: ASDisplayNode, ASGestureRecognizerDelegate { + public let contentNode: ASDisplayNode + public var contentRect: CGRect = CGRect() + private(set) var naturalContentFrame: CGRect? + + public let gesture: PinchSourceGesture + + public var isPinchGestureEnabled: Bool = true { + didSet { + if self.isPinchGestureEnabled != oldValue { + self.gesture.isEnabled = self.isPinchGestureEnabled + } + } + } + + public var maxPinchScale: CGFloat = 10.0 + + private var isActive: Bool = false + + public var activate: ((PinchSourceContainerNode) -> Void)? + public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + public var animatedOut: (() -> Void)? + public var deactivate: (() -> Void)? + public var deactivated: (() -> Void)? + public var updated: ((CGFloat, CGPoint, CGPoint) -> Void)? + + override public init() { + self.gesture = PinchSourceGesture() + self.contentNode = ASDisplayNode() + + super.init() + + self.addSubnode(self.contentNode) + + self.gesture.began = { [weak self] in + guard let strongSelf = self else { + return + } + cancelContextGestures(node: strongSelf) + cancelContextGestures(view: strongSelf.view) + strongSelf.isActive = true + + strongSelf.activate?(strongSelf) + } + + self.gesture.ended = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.isActive = false + strongSelf.deactivate?() + strongSelf.deactivated?() + } + + self.gesture.updated = { [weak self] scale, pinchLocation, offset in + guard let strongSelf = self else { + return + } + strongSelf.updated?(min(scale, strongSelf.maxPinchScale), pinchLocation, offset) + strongSelf.scaleUpdated?(min(scale, strongSelf.maxPinchScale), .immediate) + } + } + + override public func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(self.gesture) + self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in + guard let strongSelf = self else { + return false + } + return strongSelf.isActive + } + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + public func update(size: CGSize, transition: ContainedViewLayoutTransition) { + let contentFrame = CGRect(origin: CGPoint(), size: size) + self.naturalContentFrame = contentFrame + if !self.isActive { + transition.updateFrame(node: self.contentNode, frame: contentFrame) + } + } + + public func restoreToNaturalSize() { + guard let naturalContentFrame = self.naturalContentFrame else { + return + } + self.contentNode.frame = naturalContentFrame + } +} \ No newline at end of file diff --git a/submodules/Display/Source/ContextContentContainerNode.swift b/submodules/Display/Source/ContextContentContainerNode.swift index 67083e3d87..07e44a5050 100644 --- a/submodules/Display/Source/ContextContentContainerNode.swift +++ b/submodules/Display/Source/ContextContentContainerNode.swift @@ -16,7 +16,7 @@ public final class ContextContentContainerNode: ASDisplayNode { switch contentNode { case .reference: break - case .extracted: + case .extracted, .extractedContainer: break case let .controller(controller): transition.updatePosition(node: controller, position: CGPoint(x: scaledSize.width / 2.0, y: scaledSize.height / 2.0)) diff --git a/submodules/Display/Source/ContextContentSourceNode.swift b/submodules/Display/Source/ContextContentSourceNode.swift index 567bbc0664..9fae6585b4 100644 --- a/submodules/Display/Source/ContextContentSourceNode.swift +++ b/submodules/Display/Source/ContextContentSourceNode.swift @@ -39,6 +39,24 @@ public final class ContextExtractedContentContainingNode: ASDisplayNode { } } +public enum ContextExtractableContainerState { + public enum ExtractionState { + case animatedOut + case animatedIn + } + + case normal + case extracted(size: CGSize, cornerRadius: CGFloat, state: ExtractionState) +} + +public protocol ContextExtractableContainer: UIView { + typealias State = ContextExtractableContainerState + + var extractableContentView: UIView { get } + + func updateState(state: State, transition: ContainedViewLayoutTransition) +} + public final class ContextExtractedContentContainingView: UIView { public let contentView: ContextExtractedContentView public var contentRect: CGRect = CGRect() @@ -152,5 +170,6 @@ public final class ContextControllerContentNode: ASDisplayNode { public enum ContextContentNode { case reference(view: UIView) case extracted(node: ContextExtractedContentContainingNode, keepInPlace: Bool) + case extractedContainer(container: ContextExtractableContainer) case controller(ContextControllerContentNode) } diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 67ad9cb666..ef9b3fb9d1 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -192,6 +192,8 @@ public protocol NavigationBar: ASDisplayNode { func updateLayout(size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, additionalCutout: CGSize?, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool, isLandscape: Bool, transition: ContainedViewLayoutTransition) func updateEdgeEffectExtension(value: CGFloat, transition: ContainedViewLayoutTransition) + + func navigationButtonContextContainer(sourceView: UIView) -> ContextExtractableContainer? } public var defaultNavigationBarImpl: ((NavigationBarPresentationData) -> NavigationBar)? diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 225cc6ed66..52827a4718 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -989,7 +989,7 @@ private final class DrawingScreenComponent: CombinedComponent { ) ] let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) - let contextController = ContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint(x: 7.0, y: 3.0))), items: .single(ContextController.Items(content: .list(items)))) + let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint(x: 7.0, y: 3.0))), items: .single(ContextController.Items(content: .list(items)))) self.present(contextController) } @@ -3482,7 +3482,7 @@ public final class DrawingToolsInteraction { } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) - let contextController = ContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: CGRect(origin: .zero, size: CGSize(width: validLayout.size.width, height: validLayout.size.height - (validLayout.inputHeight ?? 0.0))), customPosition: CGPoint(x: 0.0, y: 1.0))), items: .single(ContextController.Items(content: .list(items)))) + let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: CGRect(origin: .zero, size: CGSize(width: validLayout.size.width, height: validLayout.size.height - (validLayout.inputHeight ?? 0.0))), customPosition: CGPoint(x: 0.0, y: 1.0))), items: .single(ContextController.Items(content: .list(items)))) self.present(contextController, .window(.root), nil) self.currentFontPicker = contextController contextController.view.disablesInteractiveKeyboardGestureRecognizer = true diff --git a/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift b/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift index 9ed63103e5..8f19178861 100644 --- a/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift +++ b/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift @@ -707,7 +707,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { return nil }, present: { [weak self] content, sourceView, sourceRect in if let strongSelf = self { - let controller = PeekController(presentationData: strongSelf.presentationData, content: content, sourceView: { + let controller = makePeekController(presentationData: strongSelf.presentationData, content: content, sourceView: { return (sourceView, sourceRect) }) strongSelf.peekController = controller diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 48fea8c8fd..5e03c08111 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -756,7 +756,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { guard let controller = self.baseNavigationController()?.topViewController as? ViewController else { return } - let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode, actionsOnTop: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode, actionsOnTop: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) controller.presentInGlobalOverlay(contextController) } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 216ad40183..31ce80f4fe 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -3254,7 +3254,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { }) } - let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode, actionsOnTop: actionsOnTop)), items: items |> map { items in + let contextController = makeContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode, actionsOnTop: actionsOnTop)), items: items |> map { items in if !items.topItems.isEmpty { return ContextController.Items(id: AnyHashable(0), content: .twoLists(items.items, items.topItems)) } else { diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift index 84c0bff8bf..fe689873f6 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift @@ -265,7 +265,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, ASScroll return nil }, present: { [weak self] content, sourceView, sourceRect in if let strongSelf = self { - let controller = PeekController(presentationData: strongSelf.presentationData, content: content, sourceView: { + let controller = makePeekController(presentationData: strongSelf.presentationData, content: content, sourceView: { return (sourceView, sourceRect) }) controller.visibilityUpdated = { [weak self] visible in diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index c289b0b52d..528b6efceb 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -628,7 +628,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { guard let strongSelf = self, let controller = strongSelf.controller else { return } - let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + let pinchController = makePinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { guard let strongSelf = self else { return CGRect() } diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index 3c39e1cd2b..4c7c859505 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -501,7 +501,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese }) }))) - let contextController = ContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, peerAction: { peer, isEnabled in let presentationData = context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift index 081116659f..e3f95ed8cf 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift @@ -518,7 +518,7 @@ public final class InviteLinkInviteController: ViewController { }))) } - let contextController = ContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.controller?.presentInGlobalOverlay(contextController) }, copyLink: { [weak self] invite in UIPasteboard.general.string = invite.link diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index 30f28682f3..bbaa98d914 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -602,7 +602,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio }))) } - let contextController = ContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, createLink: { let controller = inviteLinkEditController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: nil, completion: { invite in @@ -820,7 +820,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio }))) } - let contextController = ContextController(presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, openAdmin: { admin in let controller = inviteLinkListController(context: context, peerId: peerId, admin: admin) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index bc5f73a1e7..f226e9fd49 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -808,7 +808,7 @@ public final class InviteLinkViewController: ViewController { } } - let contextController = ContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self?.controller?.presentInGlobalOverlay(contextController) }) }) diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift index 5789250a20..8ab569cab3 100644 --- a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift +++ b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift @@ -268,7 +268,7 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio // dismissPromise.set(true) // } - let contextController = ContextController(presentationData: presentationData, source: .extracted(source), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .extracted(source), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }) }) diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsSearchItem.swift b/submodules/InviteLinksUI/Sources/InviteRequestsSearchItem.swift index 28f3932fb8..2a2b6f247e 100644 --- a/submodules/InviteLinksUI/Sources/InviteRequestsSearchItem.swift +++ b/submodules/InviteLinksUI/Sources/InviteRequestsSearchItem.swift @@ -457,7 +457,7 @@ public final class InviteRequestsSearchContainerNode: SearchDisplayControllerCon // dismissPromise.set(true) // } - let contextController = ContextController(presentationData: presentationData, source: .extracted(source), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .extracted(source), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlay(contextController) }) }) diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift index a233c6cd79..8af202a948 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift @@ -339,7 +339,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode { return } strongSelf.avatarListNode?.controlsContainerNode.alpha = 0.0 - let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + let pinchController = makePinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { return UIScreen.main.bounds }) item.context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index a561b2df94..7595e482a9 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -2388,7 +2388,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att ) self.titleView.isHighlighted = true - let contextController = ContextController( + let contextController = makeContextController( presentationData: self.presentationData, source: .reference(MediaPickerContextReferenceContentSource(controller: self, sourceView: self.titleView)), items: .single(ContextController.Items(content: .custom(content))), @@ -2923,7 +2923,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self?.presentFilePicker() }))) - let contextController = ContextController(presentationData: self.presentationData, source: .reference(MediaPickerContextReferenceContentSource(controller: self, sourceView: view)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: .reference(MediaPickerContextReferenceContentSource(controller: self, sourceView: view)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.presentInGlobalOverlay(contextController) return @@ -3084,7 +3084,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att return ContextController.Items(content: .list(items)) } - let contextController = ContextController(presentationData: self.presentationData, source: .reference(MediaPickerContextReferenceContentSource(controller: self, sourceView: view)), items: items, gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: .reference(MediaPickerContextReferenceContentSource(controller: self, sourceView: view)), items: items, gesture: gesture) self.presentInGlobalOverlay(contextController) } } diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index e309e56162..5328856fd3 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -1668,7 +1668,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta }) }))) - let contextController = ContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, manageInviteLinks: { let controller = inviteLinkListController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, admin: nil) diff --git a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift index 182c4e2dc3..28fc481656 100644 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift +++ b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift @@ -488,7 +488,7 @@ public func peersNearbyController(context: AccountContext) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil) chatController.canReadHistory.set(false) - let contextController = ContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: peerNearbyContextMenuItems(context: context, peerId: peer.id, present: { c in + let contextController = makeContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: peerNearbyContextMenuItems(context: context, peerId: peer.id, present: { c in presentControllerImpl?(c, nil) }) |> map { ContextController.Items(content: .list($0), animationCache: nil) }, gesture: gesture) presentInGlobalOverlayImpl?(contextController) diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift index cfd6ac1a60..7d21c5f759 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift @@ -422,7 +422,7 @@ public func storageUsageExceptionsScreen( let items: Signal = .single(ContextController.Items(content: .list(subItems))) let source: ContextContentSource = .reference(StorageUsageExceptionsContextReferenceContentSource(sourceView: sourceNode.labelNode.view)) - let contextController = ContextController( + let contextController = makeContextController( presentationData: presentationData, source: source, items: items, diff --git a/submodules/SettingsUI/Sources/ThemePickerController.swift b/submodules/SettingsUI/Sources/ThemePickerController.swift index 61b06a951b..475bd10a4a 100644 --- a/submodules/SettingsUI/Sources/ThemePickerController.swift +++ b/submodules/SettingsUI/Sources/ThemePickerController.swift @@ -689,7 +689,7 @@ public func themePickerController(context: AccountContext, focusOnItemTag: Theme }))) } - let contextController = ContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController, nil) }) }, colorContextAction: { isCurrent, reference, accentColor, node, gesture in @@ -938,7 +938,7 @@ public func themePickerController(context: AccountContext, focusOnItemTag: Theme } } } - let contextController = ContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController, nil) }) }) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index 4aa227f8d8..8a3574f3b0 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -779,7 +779,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The }))) } - let contextController = ContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController, nil) }) }, colorContextAction: { isCurrent, reference, accentColor, node, gesture in @@ -1028,7 +1028,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The } } } - let contextController = ContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController, nil) }) }) diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index f73982e7c3..3203932381 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -592,7 +592,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate ]) return ContextController.Items(content: .list(items), animationCache: nil) } - let contextController = ContextController(presentationData: presentationData, source: .reference(ShareContextReferenceContentSource(sourceNode: node, customPosition: CGPoint(x: 0.0, y: fromForeignApp ? -116.0 : 0.0))), items: items, gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(ShareContextReferenceContentSource(sourceNode: node, customPosition: CGPoint(x: 0.0, y: fromForeignApp ? -116.0 : 0.0))), items: items, gesture: gesture) contextController.immediateItemsTransitionAnimation = true strongSelf.present?(contextController) } diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index af215827d8..0eee76ebcc 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -2440,7 +2440,7 @@ public func channelStatsController( }) }))) - let contextController = ContextController(presentationData: presentationData, source: .extracted(ChannelStatsContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .extracted(ChannelStatsContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) controller.presentInGlobalOverlay(contextController) } dismissAllTooltipsImpl = { [weak controller] in diff --git a/submodules/StatisticsUI/Sources/MessageStatsController.swift b/submodules/StatisticsUI/Sources/MessageStatsController.swift index 3fd238a02d..2e8e688e4d 100644 --- a/submodules/StatisticsUI/Sources/MessageStatsController.swift +++ b/submodules/StatisticsUI/Sources/MessageStatsController.swift @@ -556,7 +556,7 @@ public func messageStatsController(context: AccountContext, updatedPresentationD }) }))) - let contextController = ContextController(presentationData: presentationData, source: .extracted(ChannelStatsContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .extracted(ChannelStatsContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) controller.presentInGlobalOverlay(contextController) } return controller diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 1e32fd990b..c6f960fb33 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -638,7 +638,7 @@ private final class StickerPackContainer: ASDisplayNode { if let strongSelf = self { strongSelf.hideMainPreviewIcon() - let controller = PeekController(presentationData: strongSelf.presentationData, content: content, sourceView: { + let controller = makePeekController(presentationData: strongSelf.presentationData, content: content, sourceView: { return (sourceView, sourceRect) }) controller.visibilityUpdated = { [weak self] visible in @@ -1228,7 +1228,7 @@ private final class StickerPackContainer: ASDisplayNode { }))) } - let contextController = ContextController(presentationData: self.presentationData, source: .reference(StickerPackContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: .reference(StickerPackContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.presentInGlobalOverlay(contextController, nil) } diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift index fc1b19ed9d..2e0a541e51 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -596,7 +596,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi let items = self.contextMenuSpeedItems(scheduleTooltip: { change in scheduledTooltip = change }) - let contextController = ContextController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.rateButton.referenceNode, shouldBeDismissed: self.dismissedPromise.get())), items: items, gesture: gesture) + let contextController = makeContextController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.rateButton.referenceNode, shouldBeDismissed: self.dismissedPromise.get())), items: items, gesture: gesture) contextController.dismissed = { [weak self] in if let scheduledTooltip, let self, let rate = self.playbackBaseRate { self.setRate?(rate, scheduledTooltip) diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 3a62b8af02..fb5a5a3a47 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -655,7 +655,7 @@ public final class MediaStreamComponent: CombinedComponent { } } - let contextController = ContextController(presentationData: presentationData.withUpdated(theme: defaultDarkPresentationTheme), source: .reference(ReferenceContentSource(sourceView: anchorView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + let contextController = makeContextController(presentationData: presentationData.withUpdated(theme: defaultDarkPresentationTheme), source: .reference(ReferenceContentSource(sourceView: anchorView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) controller.presentInGlobalOverlay(contextController) } ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(moreButtonTag)) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index 564a15f75b..11529cf236 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -1112,7 +1112,7 @@ final class VideoChatParticipantsComponent: Component { } self.isPinchToZoomActive = true self.state?.updated(transition: .immediate, isLocal: true) - let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + let pinchController = makePinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { return UIScreen.main.bounds }) component.call.accountContext.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift index fed8bd66dc..423d5311dd 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift @@ -596,7 +596,7 @@ extension VideoChatScreenComponent.View { } let presentationData = currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - let contextController = ContextController(presentationData: presentationData, source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + let contextController = makeContextController(presentationData: presentationData, source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) controller.presentInGlobalOverlay(contextController) } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift index 3b03188797..5bdfb5ee80 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift @@ -336,7 +336,7 @@ extension VideoChatScreenComponent.View { } let presentationData = currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - let contextController = ContextController( + let contextController = makeContextController( presentationData: presentationData, source: .extracted(ParticipantExtractedContentSource(contentView: sourceView)), items: items |> map { items in @@ -394,7 +394,7 @@ extension VideoChatScreenComponent.View { let items = itemsForEntry() let presentationData = currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - let contextController = ContextController( + let contextController = makeContextController( presentationData: presentationData, source: .extracted(ParticipantExtractedContentSource(contentView: sourceView)), items: .single(ContextController.Items(content: .list(items))), diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift index 83714a88b8..1e3a3d83db 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift @@ -904,7 +904,7 @@ final class VoiceChatMainStageNode: ASDisplayNode { } strongSelf.setControlsHidden(true, animated: false) strongSelf.controlsHidden?(true) - let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + let pinchController = makePinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { return UIScreen.main.bounds }) strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index 820b98cd76..1cc1c2f080 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -462,7 +462,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { return } strongSelf.avatarListNode?.controlsContainerNode.alpha = 0.0 - let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + let pinchController = makePinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { return UIScreen.main.bounds }) item.context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatPeerProfileNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatPeerProfileNode.swift index 9742a7f585..c3f387f679 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatPeerProfileNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatPeerProfileNode.swift @@ -99,7 +99,7 @@ final class VoiceChatPeerProfileNode: ASDisplayNode { return } strongSelf.avatarListNode.controlsContainerNode.alpha = 0.0 - let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + let pinchController = makePinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { return UIScreen.main.bounds }) context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift index c8e7d1d63f..c1042f5004 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift @@ -121,6 +121,9 @@ extension TelegramUser { if (flags2 & (1 << 16)) != 0 { botFlags.insert(.hasForum) } + if (flags2 & (1 << 17)) != 0 { + botFlags.insert(.forumManagedByUser) + } botInfo = BotUserInfo(flags: botFlags, inlinePlaceholder: botInlinePlaceholder) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift index 1006e61bd1..be2637e7d4 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift @@ -43,6 +43,7 @@ public struct BotUserInfoFlags: OptionSet { public static let isBusiness = BotUserInfoFlags(rawValue: (1 << 6)) public static let hasWebApp = BotUserInfoFlags(rawValue: (1 << 7)) public static let hasForum = BotUserInfoFlags(rawValue: (1 << 8)) + public static let forumManagedByUser = BotUserInfoFlags(rawValue: (1 << 9)) } public struct BotUserInfo: PostboxCoding, Equatable { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index c51ad45411..b650e5f2ed 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -513,6 +513,7 @@ swift_library( "//submodules/TelegramUI/Components/AlertComponent", "//submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController", "//submodules/TelegramUI/Components/CocoonInfoScreen", + "//submodules/TelegramUI/Components/ContextControllerImpl", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift b/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift index f2e5e38036..a582b3979f 100644 --- a/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift @@ -1344,7 +1344,7 @@ public class AdsInfoScreen: ViewController { }))) } - let contextController = ContextController(presentationData: presentationData, source: .reference(AdsInfoContextReferenceContentSource(controller: controller, sourceView: referenceView, insets: .zero, contentInsets: .zero)), items: .single(ContextController.Items(content: .list(actions))), gesture: nil) + let contextController = makeContextController(presentationData: presentationData, source: .reference(AdsInfoContextReferenceContentSource(controller: controller, sourceView: referenceView, insets: .zero, contentInsets: .zero)), items: .single(ContextController.Items(content: .list(actions))), gesture: nil) controller.presentInGlobalOverlay(contextController) } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraCollage.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraCollage.swift index 34eccd0846..c3539820a5 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraCollage.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraCollage.swift @@ -1227,7 +1227,7 @@ final class CameraCollageView: UIView, UIGestureRecognizerDelegate { } let items = ContextController.Items(content: .list(itemList), tip: .collageReordering) - let controller = ContextController( + let controller = makeContextController( presentationData: presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .extracted(CollageContextExtractedContentSource(contentView: sourceView)), items: .single(items), diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 3a6597782c..16219b683f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -1163,7 +1163,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { return } - let contextController = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(actions))), recognizer: recognizer, gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(actions))), recognizer: recognizer, gesture: gesture) controller.window?.presentInGlobalOverlay(contextController) } diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/BUILD b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/BUILD index 1ade4c0550..04c72e4f7b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/BUILD @@ -22,6 +22,7 @@ swift_library( "//submodules/AvatarNode", "//submodules/AccountContext", "//submodules/UndoUI", + "//submodules/TelegramUI/Components/ContextControllerImpl", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift index a6a877ccea..a0552e2033 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift @@ -12,6 +12,7 @@ import TelegramStringFormatting import AvatarNode import AccountContext import UndoUI +import ContextControllerImpl public final class ChatSendAsPeerListContextItem: ContextMenuCustomItem { let context: AccountContext diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index b28737a6e4..8942eafd60 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -1378,7 +1378,7 @@ private final class ChatSendStarsScreenComponent: Component { }))) } - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: false)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: false)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil) controller.presentInGlobalOverlay(contextController) } diff --git a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift index 872c9df914..5a2077e8aa 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift @@ -46,10 +46,10 @@ public final class ChatSideTopicsPanel: Component { case top } - public enum Kind { + public enum Kind: Equatable { case forum case monoforum - case botForum + case botForum(forumManagedByUser: Bool) } let context: AccountContext @@ -1249,8 +1249,13 @@ public final class ChatSideTopicsPanel: Component { ) let titleText: String - if case .botForum = component.kind { - titleText = component.strings.Chat_InlineTopicMenu_NewForumThreadTab + if case let .botForum(forumManagedByUser) = component.kind { + if forumManagedByUser { + titleText = component.strings.Chat_InlineTopicMenu_NewForumThreadTab + } else { + //TODO:localize + titleText = "All" + } } else { titleText = component.strings.Chat_InlineTopicMenu_AllTab } @@ -1386,8 +1391,13 @@ public final class ChatSideTopicsPanel: Component { let rightInset: CGFloat = 12.0 let titleText: String - if case .botForum = component.kind { - titleText = component.strings.Chat_InlineTopicMenu_NewForumThreadTab + if case let .botForum(forumManagedByUser) = component.kind { + if forumManagedByUser { + titleText = component.strings.Chat_InlineTopicMenu_NewForumThreadTab + } else { + //TODO:localize + titleText = "All" + } } else { titleText = component.strings.Chat_InlineTopicMenu_AllTab } @@ -2099,7 +2109,7 @@ public final class ChatSideTopicsPanel: Component { }) }))) - let contextController = ContextController( + let contextController = makeContextController( presentationData: presentationData, source: .extracted(ItemExtractedContentSource( sourceNode: sourceNode, @@ -2223,7 +2233,7 @@ public final class ChatSideTopicsPanel: Component { return } - let contextController = ContextController( + let contextController = makeContextController( presentationData: presentationData, source: .extracted(ItemExtractedContentSource( sourceNode: sourceNode, diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index 1f78c9fdd6..4be9a38665 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -4046,7 +4046,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = PeekController(presentationData: presentationData, content: content, sourceView: { + let controller = makePeekController(presentationData: presentationData, content: content, sourceView: { return (sourceView, sourceRect) }) //strongSelf.peekController = controller diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index efac328858..c9915f7505 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -2336,7 +2336,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { }))) } - let contextController = ContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.interaction?.presentGlobalOverlayController(contextController, nil) }) } @@ -3044,7 +3044,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { if let forceTheme = strongSelf.forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) } - let controller = PeekController(presentationData: presentationData, content: content, sourceView: { + let controller = makePeekController(presentationData: presentationData, content: content, sourceView: { return (sourceView, sourceRect) }) controller.visibilityUpdated = { [weak self] visible in diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 201a5677bd..cd8b140f4d 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -656,7 +656,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { let items = ContextController.Items(content: .list(itemList)) - let controller = ContextController( + let controller = makeContextController( presentationData: presentationData, source: .extracted(LinkListContextExtractedContentSource(contentView: sourceView)), items: .single(items), diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index b85ffefe2e..274dd065a0 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -725,8 +725,8 @@ public final class ChatListHeaderComponent: Component { private let leftButtonsContainer: UIView private let rightButtonsContainer: UIView - private var leftButtonsBackgroundContainer: GlassBackgroundView? - private var rightButtonsBackgroundContainer: GlassBackgroundView? + private var leftButtonsBackgroundContainer: GlassContextExtractableContainer? + private var rightButtonsBackgroundContainer: GlassContextExtractableContainer? private let storyPeerListExternalState = StoryPeerListComponent.ExternalState() private var storyPeerList: ComponentView? @@ -774,6 +774,16 @@ public final class ChatListHeaderComponent: Component { return self.storyPeerList?.view as? StoryPeerListComponent.View } + public func navigationButtonContextContainer(sourceView: UIView) -> ContextExtractableContainer? { + if let leftButtonsBackgroundContainer = self.leftButtonsBackgroundContainer, sourceView.isDescendant(of: leftButtonsBackgroundContainer) { + return leftButtonsBackgroundContainer + } + if let rightButtonsBackgroundContainer = self.rightButtonsBackgroundContainer, sourceView.isDescendant(of: rightButtonsBackgroundContainer) { + return rightButtonsBackgroundContainer + } + return nil + } + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let storyPeerListView = self.storyPeerList?.view { if let result = storyPeerListView.hitTest(self.convert(point, to: storyPeerListView), with: event) { @@ -1094,13 +1104,13 @@ public final class ChatListHeaderComponent: Component { } if leftButtonsEffectiveWidth != 0.0 { - let leftButtonsBackgroundContainer: GlassBackgroundView + let leftButtonsBackgroundContainer: GlassContextExtractableContainer var leftButtonsBackgroundContainerTransition = transition if let current = self.leftButtonsBackgroundContainer { leftButtonsBackgroundContainer = current } else { leftButtonsBackgroundContainerTransition = leftButtonsBackgroundContainerTransition.withAnimation(.none) - leftButtonsBackgroundContainer = GlassBackgroundView() + leftButtonsBackgroundContainer = GlassContextExtractableContainer() self.leftButtonsBackgroundContainer = leftButtonsBackgroundContainer self.addSubview(leftButtonsBackgroundContainer) leftButtonsBackgroundContainer.contentView.addSubview(self.leftButtonsContainer) @@ -1119,7 +1129,7 @@ public final class ChatListHeaderComponent: Component { } if rightButtonsEffectiveWidth != 0.0 { - let rightButtonsBackgroundContainer: GlassBackgroundView + let rightButtonsBackgroundContainer: GlassContextExtractableContainer var rightButtonsBackgroundContainerTransition = transition let rightButtonsContainerFrame = CGRect(origin: CGPoint(x: availableSize.width - component.sideInset - max(44.0, rightButtonsEffectiveWidth), y: 0.0), size: CGSize(width: max(44.0, rightButtonsEffectiveWidth), height: 44.0)) @@ -1128,7 +1138,7 @@ public final class ChatListHeaderComponent: Component { rightButtonsBackgroundContainer = current } else { rightButtonsBackgroundContainerTransition = rightButtonsBackgroundContainerTransition.withAnimation(.none) - rightButtonsBackgroundContainer = GlassBackgroundView() + rightButtonsBackgroundContainer = GlassContextExtractableContainer() self.rightButtonsBackgroundContainer = rightButtonsBackgroundContainer self.addSubview(rightButtonsBackgroundContainer) rightButtonsBackgroundContainer.contentView.addSubview(self.rightButtonsContainer) diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleComponent.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleComponent.swift index 3eb7f3a3ba..3e3b645024 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleComponent.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleComponent.swift @@ -115,17 +115,7 @@ public final class ChatNavigationBarTitleView: UIView, NavigationBarTitleView { let transition = ComponentTransition(transition) if let contentData = self.contentData { - let displayBackground: Bool - if let singleColor = contentData.wallpaper.singleColor { - let brightness = singleColor.brightness - if brightness <= 0.2 || brightness >= 0.8 { - displayBackground = false - } else { - displayBackground = true - } - } else { - displayBackground = true - } + let displayBackground: Bool = true let titleSize = self.title.update( transition: transition, @@ -1039,6 +1029,7 @@ public final class ChatTitleComponent: Component { if let minSubtitleWidth { contentSize.width = max(contentSize.width, minSubtitleWidth) } + contentSize.width = max(min(150.0, availableSize.width - containerSideInset * 2.0), contentSize.width) contentSize.height += subtitleSize.height let containerSize = CGSize(width: contentSize.width + containerSideInset * 2.0, height: 44.0) diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/BUILD b/submodules/TelegramUI/Components/ContextControllerImpl/BUILD new file mode 100644 index 0000000000..e88fcdb1ed --- /dev/null +++ b/submodules/TelegramUI/Components/ContextControllerImpl/BUILD @@ -0,0 +1,41 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ContextControllerImpl", + module_name = "ContextControllerImpl", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AccountContext", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/AnimationUI", + "//submodules/AppBundle", + "//submodules/AsyncDisplayKit", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/ComponentFlow", + "//submodules/ContextUI", + "//submodules/Display", + "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/EntityKeyboard", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/Markdown", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/ReactionSelectionNode", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TextSelectionNode", + "//submodules/UIKitRuntimeUtils", + "//submodules/UndoUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/ContextUI/Sources/ContextActionNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionNode.swift similarity index 98% rename from submodules/ContextUI/Sources/ContextActionNode.swift rename to submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionNode.swift index 7fc66b761d..c086d6d1cb 100644 --- a/submodules/ContextUI/Sources/ContextActionNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionNode.swift @@ -5,19 +5,7 @@ import Display import TelegramPresentationData import SwiftSignalKit import Markdown - -public enum ContextActionSibling { - case none - case item - case separator -} - -public protocol ContextActionNodeProtocol: ASDisplayNode { - func setIsHighlighted(_ value: Bool) - func performAction() - func actionNode(at point: CGPoint) -> ContextActionNodeProtocol - var isActionEnabled: Bool { get } -} +import ContextUI public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol { private var presentationData: PresentationData diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift similarity index 99% rename from submodules/ContextUI/Sources/ContextActionsContainerNode.swift rename to submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift index f2ff84a502..2602854638 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift @@ -10,6 +10,7 @@ import AppBundle import TextFormat import TextNodeWithEntities import SwiftSignalKit +import ContextUI private final class ContextActionsSelectionGestureRecognizer: UIPanGestureRecognizer { var updateLocation: ((CGPoint, Bool) -> Void)? diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerActionsStackNode.swift similarity index 95% rename from submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift rename to submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerActionsStackNode.swift index e0c71fabe3..68ce078274 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerActionsStackNode.swift @@ -18,76 +18,7 @@ import ComponentDisplayAdapters import GlassBackgroundComponent import LottieComponent import TextNodeWithEntities - -public protocol ContextControllerActionsStackItemNode: ASDisplayNode { - var wantsFullWidth: Bool { get } - - func update( - presentationData: PresentationData, - constrainedSize: CGSize, - standardMinWidth: CGFloat, - standardMaxWidth: CGFloat, - additionalBottomInset: CGFloat, - transition: ContainedViewLayoutTransition - ) -> (size: CGSize, apparentHeight: CGFloat) - - func highlightGestureMoved(location: CGPoint) - func highlightGestureFinished(performAction: Bool) - - func decreaseHighlightedIndex() - func increaseHighlightedIndex() -} - -public struct ContextControllerReactionItems { - public var context: AccountContext - public var reactionItems: [ReactionContextItem] - public var selectedReactionItems: Set - public var reactionsTitle: String? - public var reactionsLocked: Bool - public var animationCache: AnimationCache - public var alwaysAllowPremiumReactions: Bool - public var allPresetReactionsAreAvailable: Bool - public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? - - public init(context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, reactionsTitle: String?, reactionsLocked: Bool, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, allPresetReactionsAreAvailable: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?) { - self.context = context - self.reactionItems = reactionItems - self.selectedReactionItems = selectedReactionItems - self.reactionsTitle = reactionsTitle - self.reactionsLocked = reactionsLocked - self.animationCache = animationCache - self.alwaysAllowPremiumReactions = alwaysAllowPremiumReactions - self.allPresetReactionsAreAvailable = allPresetReactionsAreAvailable - self.getEmojiContent = getEmojiContent - } -} - -public final class ContextControllerPreviewReaction { - public let context: AccountContext - public let file: TelegramMediaFile - - public init(context: AccountContext, file: TelegramMediaFile) { - self.context = context - self.file = file - } -} - -public protocol ContextControllerActionsStackItem: AnyObject { - func node( - context: AccountContext?, - getController: @escaping () -> ContextControllerProtocol?, - requestDismiss: @escaping (ContextMenuActionResult) -> Void, - requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, - requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void - ) -> ContextControllerActionsStackItemNode - - var id: AnyHashable? { get } - var tip: ContextController.Tip? { get } - var tipSignal: Signal? { get } - var reactionItems: ContextControllerReactionItems? { get } - var previewReaction: ContextControllerPreviewReaction? { get } - var dismissed: (() -> Void)? { get } -} +import ContextUI public protocol ContextControllerActionsListItemNode: ASDisplayNode { func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void) @@ -1326,17 +1257,11 @@ private final class ItemSelectionRecognizer: UIGestureRecognizer { } } - -public final class ContextControllerActionsStackNode: ASDisplayNode { - public enum Presentation { - case modal - case inline - case additional - } - +public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, ContextControllerActionsStackNode { final class NavigationContainer: ASDisplayNode, ASGestureRecognizerDelegate { let backgroundContainer: GlassBackgroundContainerView let backgroundView: GlassBackgroundView + var sourceExtractableContainer: ContextExtractableContainer? let contentContainer: UIView var requestUpdate: ((ContainedViewLayoutTransition) -> Void)? @@ -1417,6 +1342,55 @@ public final class ContextControllerActionsStackNode: ASDisplayNode { } } + func animateIn(fromExtractableContainer extractableContainer: ContextExtractableContainer) { + let transition: ComponentTransition = .spring(duration: 0.42) + + let normalSize = extractableContainer.extractableContentView.bounds.size + let normalCornerRadius: CGFloat = min(normalSize.width, normalSize.height) * 0.5 + + self.sourceExtractableContainer = extractableContainer + self.backgroundView.isHidden = true + + self.backgroundContainer.contentView.addSubview(extractableContainer.extractableContentView) + for subview in extractableContainer.extractableContentView.subviews { + if let subview = subview as? GlassBackgroundView { + //TODO:release + subview.contentView.addSubview(self.contentContainer) + break + } + } + + self.sourceExtractableContainer = nil + self.contentContainer.frame = CGRect(origin: CGPoint(), size: normalSize) + self.contentContainer.layer.cornerRadius = normalCornerRadius + + transition.setFrame(view: self.contentContainer, frame: CGRect(origin: CGPoint(), size: self.backgroundContainer.bounds.size)) + transition.setCornerRadius(layer: self.contentContainer.layer, cornerRadius: 30.0) + self.contentContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + + extractableContainer.updateState(state: .extracted(size: normalSize, cornerRadius: normalCornerRadius, state: .animatedOut), transition: .immediate) + extractableContainer.updateState(state: .extracted(size: self.backgroundContainer.bounds.size, cornerRadius: 30.0, state: .animatedIn), transition: transition.containedViewLayoutTransition) + } + + func animateOut(toExtractableContainer extractableContainer: ContextExtractableContainer, transition: ContainedViewLayoutTransition) { + let transition = ComponentTransition(transition) + + let normalSize = extractableContainer.extractableContentView.bounds.size + let normalCornerRadius: CGFloat = min(normalSize.width, normalSize.height) * 0.5 + + transition.setFrame(view: self.contentContainer, frame: CGRect(origin: CGPoint(), size: normalSize)) + transition.attachAnimation(view: self.contentContainer, id: "animateOut", completion: { [weak extractableContainer] _ in + guard let extractableContainer else { + return + } + extractableContainer.addSubview(extractableContainer.extractableContentView) + }) + transition.setCornerRadius(layer: self.contentContainer.layer, cornerRadius: normalCornerRadius) + transition.setAlpha(view: self.contentContainer, alpha: 0.0) + + extractableContainer.updateState(state: .normal, transition: transition.containedViewLayoutTransition) + } + func update(presentationData: PresentationData, presentation: Presentation, size: CGSize, transition: ContainedViewLayoutTransition) { let transition = ComponentTransition(transition) @@ -1443,7 +1417,12 @@ public final class ContextControllerActionsStackNode: ASDisplayNode { } transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) - self.backgroundView.update(size: size, cornerRadius: 30.0, isDark: presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: presentationData.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: false, transition: transition) + self.backgroundView.update(size: size, cornerRadius: 30.0, isDark: presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: presentationData.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition) + + if let sourceExtractableContainer = self.sourceExtractableContainer { + transition.setFrame(view: sourceExtractableContainer.extractableContentView, frame: CGRect(origin: CGPoint(), size: size)) + sourceExtractableContainer.updateState(state: .extracted(size: size, cornerRadius: 30.0, state: .animatedIn), transition: transition.containedViewLayoutTransition) + } } } @@ -2080,4 +2059,12 @@ public final class ContextControllerActionsStackNode: ASDisplayNode { } } } + + func animateIn(fromExtractableContainer extractableContainer: ContextExtractableContainer) { + self.navigationContainer.animateIn(fromExtractableContainer: extractableContainer) + } + + func animateOut(toExtractableContainer extractableContainer: ContextExtractableContainer, transition: ContainedViewLayoutTransition) { + self.navigationContainer.animateOut(toExtractableContainer: extractableContainer, transition: transition) + } } diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift similarity index 92% rename from submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift rename to submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift index 0fda18d4a3..b0df255488 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift @@ -9,6 +9,9 @@ import SwiftSignalKit import ReactionSelectionNode import UndoUI import AccountContext +import ContextUI +import ComponentFlow +import ComponentDisplayAdapters private extension ContextControllerTakeViewInfo.ContainingItem { var contentRect: CGRect { @@ -264,7 +267,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo private let contentRectDebugNode: ASDisplayNode private var actionsContainerNode: ASDisplayNode - private let actionsStackNode: ContextControllerActionsStackNode + private let actionsStackNode: ContextControllerActionsStackNodeImpl private let additionalActionsStackNode: ContextControllerActionsStackNode private var validLayout: ContainerViewLayout? @@ -324,7 +327,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.contentRectDebugNode.backgroundColor = UIColor.red.withAlphaComponent(0.2) self.actionsContainerNode = ASDisplayNode() - self.actionsStackNode = ContextControllerActionsStackNode( + self.actionsStackNode = ContextControllerActionsStackNodeImpl( context: self.context, getController: getController, requestDismiss: { result in @@ -333,7 +336,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo requestUpdate: requestUpdate ) - self.additionalActionsStackNode = ContextControllerActionsStackNode( + self.additionalActionsStackNode = ContextControllerActionsStackNodeImpl( context: self.context, getController: getController, requestDismiss: { result in @@ -662,7 +665,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo var contentTopInset: CGFloat = topInset var removedReactionContextNode: ReactionContextNode? - if let reactionItems = self.actionsStackNode.topReactionItems, !reactionItems.reactionItems.isEmpty, let controller = self.getController() as? ContextController { + if let reactionItems = self.actionsStackNode.topReactionItems, !reactionItems.reactionItems.isEmpty, let controller = self.getController() as? ContextControllerImpl { let reactionContextNode: ReactionContextNode if let current = self.reactionContextNode { reactionContextNode = current @@ -707,14 +710,14 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } reactionContextNode.reactionSelected = { [weak self] reaction, isLarge in - guard let strongSelf = self, let controller = strongSelf.getController() as? ContextController else { + guard let strongSelf = self, let controller = strongSelf.getController() as? ContextControllerImpl else { return } controller.reactionSelected?(reaction, isLarge) } let context = reactionItems.context reactionContextNode.premiumReactionsSelected = { [weak self] file in - guard let strongSelf = self, let validLayout = strongSelf.validLayout, let controller = strongSelf.getController() as? ContextController else { + guard let strongSelf = self, let validLayout = strongSelf.validLayout, let controller = strongSelf.getController() as? ContextControllerImpl else { return } @@ -799,6 +802,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo var contentRect: CGRect var isContentResizeableVertically: Bool = false let _ = isContentResizeableVertically + var contextExtractableContainer: (container: ContextExtractableContainer, sourceRect: CGRect)? switch self.source { case let .location(location): @@ -810,6 +814,10 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } case let .reference(reference): if let transitionInfo = reference.transitionInfo() { + if let referenceView = transitionInfo.referenceView as? ContextExtractableContainer { + contextExtractableContainer = (referenceView, convertFrame(transitionInfo.referenceView.bounds.inset(by: transitionInfo.insets), from: transitionInfo.referenceView, to: self.view)) + } + contentRect = convertFrame(transitionInfo.referenceView.bounds.inset(by: transitionInfo.insets), from: transitionInfo.referenceView, to: self.view).insetBy(dx: -2.0, dy: 0.0) contentRect.size.width += 5.0 contentParentGlobalFrame = CGRect(origin: CGPoint(x: 0.0, y: contentRect.minY), size: CGSize(width: layout.size.width, height: contentRect.height)) @@ -1051,14 +1059,17 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect, beginWithCurrentState: true) var actionsFrame: CGRect - if case let .reference(source) = self.source, let actionsPosition = source.transitionInfo()?.actionsPosition, case .top = actionsPosition { + if let contextExtractableContainer { + let _ = contextExtractableContainer + actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.minY), size: actionsSize) + } else if case let .reference(source) = self.source, let actionsPosition = source.transitionInfo()?.actionsPosition, case .top = actionsPosition { actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.minY - contentActionsSpacing - actionsSize.height), size: actionsSize) } else { actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.maxY + contentActionsSpacing), size: actionsSize) } var contentVerticalOffset: CGFloat = 0.0 - if keepInPlace, case .extracted = self.source { + if contextExtractableContainer == nil, keepInPlace, case .extracted = self.source { actionsFrame.origin.y = contentRect.minY - contentActionsSpacing - actionsFrame.height let statusBarHeight = (layout.statusBarHeight ?? 0.0) if actionsFrame.origin.y < statusBarHeight { @@ -1333,52 +1344,65 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo currentContentScreenFrame = contentRect } - self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: self.actionsContainerNode.alpha, duration: 0.05) - self.actionsContainerNode.layer.animateSpring( - from: 0.01 as NSNumber, - to: 1.0 as NSNumber, - keyPath: "transform.scale", - duration: duration, - delay: 0.0, - initialVelocity: 0.0, - damping: springDamping, - additive: false - ) - - var actionsPositionDeltaXDistance: CGFloat = 0.0 - if case .center = actionsHorizontalAlignment { - actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX - } - - if case .reference = self.source { - actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX - } - - let actionsVerticalTransitionDirection: CGFloat - if let contentNode = itemContentNode { - if contentNode.frame.minY < self.actionsContainerNode.frame.minY { - actionsVerticalTransitionDirection = -1.0 - } else { - actionsVerticalTransitionDirection = 1.0 - } + if let contextExtractableContainer { + let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: .spring) + + transition.animatePositionAdditive(layer: self.actionsContainerNode.layer, offset: CGPoint( + x: contextExtractableContainer.sourceRect.minX - self.actionsContainerNode.frame.minX, + y: contextExtractableContainer.sourceRect.minY - self.actionsContainerNode.frame.minY + )) + + self.actionsStackNode.animateIn(fromExtractableContainer: contextExtractableContainer.container) } else { - if contentRect.minY < self.actionsContainerNode.frame.minY { - actionsVerticalTransitionDirection = -1.0 - } else { - actionsVerticalTransitionDirection = 1.0 + self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: self.actionsContainerNode.alpha, duration: 0.05) + self.actionsContainerNode.layer.animateSpring( + from: 0.01 as NSNumber, + to: 1.0 as NSNumber, + keyPath: "transform.scale", + duration: duration, + delay: 0.0, + initialVelocity: 0.0, + damping: springDamping, + additive: false + ) + + var actionsPositionDeltaXDistance: CGFloat = 0.0 + if case .center = actionsHorizontalAlignment { + actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX } + + if case .reference = self.source { + actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX + } + + let actionsVerticalTransitionDirection: CGFloat + if let contentNode = itemContentNode { + if contentNode.frame.minY < self.actionsContainerNode.frame.minY { + actionsVerticalTransitionDirection = -1.0 + } else { + actionsVerticalTransitionDirection = 1.0 + } + } else { + if contentRect.minY < self.actionsContainerNode.frame.minY { + actionsVerticalTransitionDirection = -1.0 + } else { + actionsVerticalTransitionDirection = 1.0 + } + } + let actionsPositionDeltaYDistance = -animationInContentYDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing + self.actionsContainerNode.layer.animateSpring( + from: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)), + to: NSValue(cgPoint: CGPoint()), + keyPath: "position", + duration: duration, + delay: 0.0, + initialVelocity: 0.0, + damping: springDamping, + additive: true + ) + + self.actionsStackNode.animateIn() } - let actionsPositionDeltaYDistance = -animationInContentYDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing - self.actionsContainerNode.layer.animateSpring( - from: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)), - to: NSValue(cgPoint: CGPoint()), - keyPath: "position", - duration: duration, - delay: 0.0, - initialVelocity: 0.0, - damping: springDamping, - additive: true - ) if let reactionContextNode = self.reactionContextNode { let reactionsPositionDeltaYDistance = -animationInContentYDistance @@ -1395,8 +1419,6 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo reactionContextNode.animateIn(from: currentContentScreenFrame) } - self.actionsStackNode.animateIn() - if let contentNode = itemContentNode { contentNode.containingItem.isExtractedToContextPreview = true contentNode.containingItem.isExtractedToContextPreviewUpdated?(true) @@ -1678,41 +1700,55 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo ) } - self.actionsContainerNode.layer.animateAlpha(from: self.actionsContainerNode.alpha, to: 0.0, duration: duration, removeOnCompletion: false) - self.actionsContainerNode.layer.animate( - from: 1.0 as NSNumber, - to: 0.01 as NSNumber, - keyPath: "transform.scale", - timingFunction: timingFunction, - duration: duration, - delay: 0.0, - removeOnCompletion: false, - completion: { _ in + if let contextExtractableContainer { + let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: .easeInOut) + + transition.updateFrame(node: self.actionsContainerNode, frame: CGRect(origin: CGPoint(x: contextExtractableContainer.sourceRect.minX, y: contextExtractableContainer.sourceRect.minY), size: self.actionsContainerNode.bounds.size)) + ComponentTransition(transition).attachAnimation(view: self.actionsContainerNode.view, id: "animateOut", completion: { _ in if completeWithActionStack { restoreOverlayViews.forEach({ $0() }) completion() } + }) + + self.actionsStackNode.animateOut(toExtractableContainer: contextExtractableContainer.container, transition: transition) + } else { + self.actionsContainerNode.layer.animateAlpha(from: self.actionsContainerNode.alpha, to: 0.0, duration: duration, removeOnCompletion: false) + self.actionsContainerNode.layer.animate( + from: 1.0 as NSNumber, + to: 0.01 as NSNumber, + keyPath: "transform.scale", + timingFunction: timingFunction, + duration: duration, + delay: 0.0, + removeOnCompletion: false, + completion: { _ in + if completeWithActionStack { + restoreOverlayViews.forEach({ $0() }) + completion() + } + } + ) + + var actionsPositionDeltaXDistance: CGFloat = 0.0 + if case .center = actionsHorizontalAlignment { + actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX } - ) - - var actionsPositionDeltaXDistance: CGFloat = 0.0 - if case .center = actionsHorizontalAlignment { - actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX + if case .reference = self.source { + actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX + } + let actionsPositionDeltaYDistance = -animationInContentYDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing + self.actionsContainerNode.layer.animate( + from: NSValue(cgPoint: CGPoint()), + to: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)), + keyPath: "position", + timingFunction: timingFunction, + duration: duration, + delay: 0.0, + removeOnCompletion: false, + additive: true + ) } - if case .reference = self.source { - actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX - } - let actionsPositionDeltaYDistance = -animationInContentYDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing - self.actionsContainerNode.layer.animate( - from: NSValue(cgPoint: CGPoint()), - to: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)), - keyPath: "position", - timingFunction: timingFunction, - duration: duration, - delay: 0.0, - removeOnCompletion: false, - additive: true - ) if let reactionContextNode = self.reactionContextNode { reactionContextNode.animateOut(to: currentContentScreenFrame, animatingOutToReaction: self.reactionContextNodeIsAnimatingOut) diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerImpl.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerImpl.swift new file mode 100644 index 0000000000..0dec528b25 --- /dev/null +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerImpl.swift @@ -0,0 +1,2203 @@ +import Foundation +import UIKit +import Display +import ContextUI +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import AsyncDisplayKit +import ReactionSelectionNode +import TelegramCore +import UIKitRuntimeUtils +import UndoUI +import TextSelectionNode + +private let animationDurationFactor: Double = 1.0 + +func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect { + let sourceWindowFrame = fromView.convert(frame, to: nil) + var targetWindowFrame = toView.convert(sourceWindowFrame, from: nil) + + if let fromWindow = fromView.window, let toWindow = toView.window { + targetWindowFrame.origin.x += toWindow.bounds.width - fromWindow.bounds.width + } + return targetWindowFrame +} + +final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelegate { + private weak var controller: ContextControllerImpl? + private let context: AccountContext? + private var presentationData: PresentationData + + private let configuration: ContextController.Configuration + + private let legacySource: ContextContentSource + private var legacyItems: Signal + + let beginDismiss: (ContextMenuActionResult) -> Void + private let beganAnimatingOut: () -> Void + private let attemptTransitionControllerIntoNavigation: () -> Void + var dismissedForCancel: (() -> Void)? + private let getController: () -> ContextControllerProtocol? + private weak var gesture: ContextGesture? + + private var didSetItemsReady = false + let itemsReady = Promise() + let contentReady = Promise() + + private var currentItems: ContextController.Items? + private var currentActionsMinHeight: ContextController.ActionsHeight? + + private var validLayout: ContainerViewLayout? + + private let effectView: UIVisualEffectView + private var propertyAnimator: AnyObject? + private var displayLinkAnimator: DisplayLinkAnimator? + private let dimNode: ASDisplayNode + private let withoutBlurDimNode: ASDisplayNode + private let dismissNode: ASDisplayNode + private let dismissAccessibilityArea: AccessibilityAreaNode + + private var sourceContainer: ContextSourceContainer? + + private let clippingNode: ASDisplayNode + private let scrollNode: ASScrollNode + + private var originalProjectedContentViewFrame: (CGRect, CGRect)? + private var contentAreaInScreenSpace: CGRect? + private var customPosition: CGPoint? + private let contentContainerNode: ContextContentContainerNode + private var actionsContainerNode: ContextActionsContainerNode + + private var didCompleteAnimationIn = false + private var initialContinueGesturePoint: CGPoint? + private var didMoveFromInitialGesturePoint = false + private var highlightedActionNode: ContextActionNodeProtocol? + private var highlightedReaction: ReactionItem.Reaction? + + private let hapticFeedback = HapticFeedback() + + private var animatedIn = false + private var isAnimatingOut = false + + private let itemsDisposable = MetaDisposable() + + private let blurBackground: Bool + + var overlayWantsToBeBelowKeyboard: Bool { + guard let sourceContainer = self.sourceContainer else { + return false + } + return sourceContainer.overlayWantsToBeBelowKeyboard + } + + init( + controller: ContextControllerImpl, + context: AccountContext?, + presentationData: PresentationData, + configuration: ContextController.Configuration, + beginDismiss: @escaping (ContextMenuActionResult) -> Void, + recognizer: TapLongTapOrDoubleTapGestureRecognizer?, + gesture: ContextGesture?, + beganAnimatingOut: @escaping () -> Void, + attemptTransitionControllerIntoNavigation: @escaping () -> Void + ) { + self.controller = controller + self.context = context + self.presentationData = presentationData + self.configuration = configuration + self.beginDismiss = beginDismiss + self.beganAnimatingOut = beganAnimatingOut + self.attemptTransitionControllerIntoNavigation = attemptTransitionControllerIntoNavigation + self.gesture = gesture + + self.legacySource = configuration.sources[0].source + self.legacyItems = configuration.sources[0].items + + self.getController = { [weak controller] in + return controller + } + + self.effectView = UIVisualEffectView() + if #available(iOS 9.0, *) { + } else { + if presentationData.theme.rootController.keyboardColor == .dark { + self.effectView.effect = UIBlurEffect(style: .dark) + } else { + self.effectView.effect = UIBlurEffect(style: .light) + } + self.effectView.alpha = 0.0 + } + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = presentationData.theme.contextMenu.dimColor + self.dimNode.alpha = 0.0 + + self.withoutBlurDimNode = ASDisplayNode() + self.withoutBlurDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.4) + self.withoutBlurDimNode.alpha = 0.0 + + self.dismissNode = ASDisplayNode() + self.dismissAccessibilityArea = AccessibilityAreaNode() + self.dismissAccessibilityArea.accessibilityLabel = presentationData.strings.VoiceOver_DismissContextMenu + self.dismissAccessibilityArea.accessibilityTraits = .button + + self.clippingNode = ASDisplayNode() + self.clippingNode.clipsToBounds = true + + self.scrollNode = ASScrollNode() + self.scrollNode.canCancelAllTouchesInViews = true + self.scrollNode.view.delaysContentTouches = false + self.scrollNode.view.showsVerticalScrollIndicator = false + if #available(iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + + self.contentContainerNode = ContextContentContainerNode() + + var feedbackTap: (() -> Void)? + var updateLayout: (() -> Void)? + + var blurBackground = true + if let mainSource = configuration.sources.first(where: { $0.id == configuration.initialId }) { + if case .reference = mainSource.source { + blurBackground = false + } else if case let .extracted(extractedSource) = mainSource.source, !extractedSource.blurBackground { + blurBackground = false + } + } + self.blurBackground = blurBackground + + self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: ContextController.Items(), getController: { [weak controller] in + return controller + }, actionSelected: { result in + beginDismiss(result) + }, requestLayout: { + updateLayout?() + }, feedbackTap: { + feedbackTap?() + }, blurBackground: blurBackground) + + super.init() + + feedbackTap = { [weak self] in + self?.hapticFeedback.tap() + } + + updateLayout = { [weak self] in + self?.updateLayout() + } + + self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + + if blurBackground { + self.view.addSubview(self.effectView) + self.addSubnode(self.dimNode) + self.addSubnode(self.withoutBlurDimNode) + } + + self.addSubnode(self.clippingNode) + + self.clippingNode.addSubnode(self.scrollNode) + self.scrollNode.addSubnode(self.dismissNode) + self.scrollNode.addSubnode(self.dismissAccessibilityArea) + + self.scrollNode.addSubnode(self.actionsContainerNode) + + if let recognizer = recognizer { + recognizer.externalUpdated = { [weak self, weak recognizer] view, point in + guard let strongSelf = self, let _ = recognizer else { + return + } + let localPoint = strongSelf.view.convert(point, from: view) + let initialPoint: CGPoint + if let current = strongSelf.initialContinueGesturePoint { + initialPoint = current + } else { + initialPoint = localPoint + strongSelf.initialContinueGesturePoint = localPoint + } + if strongSelf.didCompleteAnimationIn { + if !strongSelf.didMoveFromInitialGesturePoint { + let distance = abs(localPoint.y - initialPoint.y) + if distance > 12.0 { + strongSelf.didMoveFromInitialGesturePoint = true + } + } + if strongSelf.didMoveFromInitialGesturePoint { + if let sourceContainer = strongSelf.sourceContainer { + let presentationPoint = strongSelf.view.convert(localPoint, to: sourceContainer.view) + sourceContainer.highlightGestureMoved(location: presentationPoint, hover: false) + } else { + let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) + let actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) + if strongSelf.highlightedActionNode !== actionNode { + strongSelf.highlightedActionNode?.setIsHighlighted(false) + strongSelf.highlightedActionNode = actionNode + if let actionNode = actionNode { + actionNode.setIsHighlighted(true) + strongSelf.hapticFeedback.tap() + } + } + } + } + } + } + recognizer.externalEnded = { [weak self, weak recognizer] viewAndPoint in + guard let strongSelf = self, let recognizer = recognizer else { + return + } + recognizer.externalUpdated = nil + if strongSelf.didMoveFromInitialGesturePoint { + if let sourceContainer = strongSelf.sourceContainer { + sourceContainer.highlightGestureFinished(performAction: viewAndPoint != nil) + } else { + if let (_, _) = viewAndPoint { + if let highlightedActionNode = strongSelf.highlightedActionNode { + strongSelf.highlightedActionNode = nil + highlightedActionNode.performAction() + } + } else { + if let highlightedActionNode = strongSelf.highlightedActionNode { + strongSelf.highlightedActionNode = nil + highlightedActionNode.setIsHighlighted(false) + } + } + } + } + } + } else if let gesture = gesture { + gesture.externalUpdated = { [weak self, weak gesture] view, point in + guard let strongSelf = self, let _ = gesture else { + return + } + let localPoint: CGPoint + if let layout = strongSelf.validLayout, layout.metrics.isTablet, layout.size.width > layout.size.height, let view { + localPoint = view.convert(point, to: nil) + } else { + localPoint = strongSelf.view.convert(point, from: view) + } + let initialPoint: CGPoint + if let current = strongSelf.initialContinueGesturePoint { + initialPoint = current + } else { + initialPoint = localPoint + strongSelf.initialContinueGesturePoint = localPoint + } + if strongSelf.didCompleteAnimationIn { + if !strongSelf.didMoveFromInitialGesturePoint { + let distance = abs(localPoint.y - initialPoint.y) + if distance > 4.0 { + strongSelf.didMoveFromInitialGesturePoint = true + } + } + if strongSelf.didMoveFromInitialGesturePoint { + if let sourceContainer = strongSelf.sourceContainer { + let presentationPoint = strongSelf.view.convert(localPoint, to: sourceContainer.view) + sourceContainer.highlightGestureMoved(location: presentationPoint, hover: false) + } else { + let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) + var actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) + if let actionNodeValue = actionNode, !actionNodeValue.isActionEnabled { + actionNode = nil + } + + if strongSelf.highlightedActionNode !== actionNode { + strongSelf.highlightedActionNode?.setIsHighlighted(false) + strongSelf.highlightedActionNode = actionNode + if let actionNode = actionNode { + actionNode.setIsHighlighted(true) + strongSelf.hapticFeedback.tap() + } + } + } + } + } + } + gesture.externalEnded = { [weak self, weak gesture] viewAndPoint in + guard let strongSelf = self, let gesture = gesture else { + return + } + gesture.externalUpdated = nil + if strongSelf.didMoveFromInitialGesturePoint { + if let sourceContainer = strongSelf.sourceContainer { + sourceContainer.highlightGestureFinished(performAction: viewAndPoint != nil) + } else { + if let (_, _) = viewAndPoint { + if let highlightedActionNode = strongSelf.highlightedActionNode { + strongSelf.highlightedActionNode = nil + highlightedActionNode.performAction() + } + } else { + if let highlightedActionNode = strongSelf.highlightedActionNode { + strongSelf.highlightedActionNode = nil + highlightedActionNode.setIsHighlighted(false) + } + } + } + } + } + } + + self.initializeContent() + + self.dismissAccessibilityArea.activate = { [weak self] in + self?.dimNodeTapped() + return true + } + + if controller.disableScreenshots { + setLayerDisableScreenshots(self.layer, true) + } + } + + deinit { + if let propertyAnimator = self.propertyAnimator { + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator + propertyAnimator?.stopAnimation(true) + } + } + + self.itemsDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + self.dismissNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTapped))) + + if #available(iOS 13.0, *) { + self.view.addGestureRecognizer(UIHoverGestureRecognizer(target: self, action: #selector(self.hoverGesture(_:)))) + } + } + + @objc private func dimNodeTapped() { + guard self.animatedIn else { + return + } + self.dismissedForCancel?() + self.beginDismiss(.default) + } + + @available(iOS 13.0, *) + @objc private func hoverGesture(_ gestureRecognizer: UIHoverGestureRecognizer) { + guard self.didCompleteAnimationIn else { + return + } + + let localPoint = gestureRecognizer.location(in: self.view) + + switch gestureRecognizer.state { + case .changed: + if let sourceContainer = self.sourceContainer { + let presentationPoint = self.view.convert(localPoint, to: sourceContainer.view) + sourceContainer.highlightGestureMoved(location: presentationPoint, hover: true) + } else { + let actionPoint = self.view.convert(localPoint, to: self.actionsContainerNode.view) + let actionNode = self.actionsContainerNode.actionNode(at: actionPoint) + if self.highlightedActionNode !== actionNode { + self.highlightedActionNode?.setIsHighlighted(false) + self.highlightedActionNode = actionNode + if let actionNode = actionNode { + actionNode.setIsHighlighted(true) + } + } + } + case .ended, .cancelled: + if let sourceContainer = self.sourceContainer { + sourceContainer.highlightGestureMoved(location: CGPoint(x: -1, y: -1), hover: true) + } else { + if let highlightedActionNode = self.highlightedActionNode { + self.highlightedActionNode = nil + highlightedActionNode.setIsHighlighted(false) + } + } + default: + break + } + } + + private func initializeContent() { + if self.configuration.sources.count == 1 { + switch self.configuration.sources[0].source { + case .location: + break + case let .reference(source): + if let controller = self.getController() as? ContextControllerImpl, controller.workaroundUseLegacyImplementation { + self.contentReady.set(.single(true)) + + let transitionInfo = source.transitionInfo() + if let transitionInfo { + let referenceView = transitionInfo.referenceView + self.contentContainerNode.contentNode = .reference(view: referenceView) + self.contentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace + self.customPosition = transitionInfo.customPosition + + var projectedFrame = convertFrame(referenceView.bounds, from: referenceView, to: self.view) + projectedFrame.origin.x += transitionInfo.insets.left + projectedFrame.size.width -= transitionInfo.insets.left + transitionInfo.insets.right + projectedFrame.origin.y += transitionInfo.insets.top + projectedFrame.size.width -= transitionInfo.insets.top + transitionInfo.insets.bottom + self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) + } + + self.itemsDisposable.set((self.configuration.sources[0].items + |> deliverOnMainQueue).start(next: { [weak self] items in + self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale) + })) + + return + } + case .extracted: + break + case let .controller(source): + if let controller = self.getController() as? ContextControllerImpl, controller.workaroundUseLegacyImplementation { + self.contentReady.set(source.controller.ready.get()) + + let transitionInfo = source.transitionInfo() + if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() { + let contentParentNode = ContextControllerContentNode(sourceView: sourceView, controller: source.controller, tapped: { [weak self] in + self?.attemptTransitionControllerIntoNavigation() + }) + self.contentContainerNode.contentNode = .controller(contentParentNode) + self.scrollNode.addSubnode(self.contentContainerNode) + self.contentContainerNode.clipsToBounds = true + self.contentContainerNode.cornerRadius = 14.0 + self.contentContainerNode.addSubnode(contentParentNode) + + let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view) + self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) + } + + self.itemsDisposable.set((self.configuration.sources[0].items + |> deliverOnMainQueue).start(next: { [weak self] items in + self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale) + })) + + return + } + } + } + + if let controller = self.controller { + let sourceContainer = ContextSourceContainer(controller: controller, configuration: self.configuration, context: self.context) + self.contentReady.set(sourceContainer.ready.get()) + self.itemsReady.set(.single(true)) + self.sourceContainer = sourceContainer + self.addSubnode(sourceContainer) + } + } + + func animateIn() { + self.gesture?.endPressedAppearance() + self.hapticFeedback.impact() + + if let sourceContainer = self.sourceContainer { + self.didCompleteAnimationIn = true + sourceContainer.animateIn() + return + } + + switch self.legacySource { + case .location, .reference: + break + case .extracted: + if let contentAreaInScreenSpace = self.contentAreaInScreenSpace, let maybeContentNode = self.contentContainerNode.contentNode, case .extracted = maybeContentNode { + var updatedContentAreaInScreenSpace = contentAreaInScreenSpace + updatedContentAreaInScreenSpace.origin.x = 0.0 + updatedContentAreaInScreenSpace.size.width = self.bounds.width + + self.clippingNode.layer.animateFrame(from: updatedContentAreaInScreenSpace, to: self.clippingNode.frame, duration: 0.18 * animationDurationFactor, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + self.clippingNode.layer.animateBoundsOriginYAdditive(from: updatedContentAreaInScreenSpace.minY, to: 0.0, duration: 0.18 * animationDurationFactor, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + case let .controller(source): + let transitionInfo = source.transitionInfo() + if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() { + let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view) + self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) + + var updatedContentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace + updatedContentAreaInScreenSpace.origin.x = 0.0 + updatedContentAreaInScreenSpace.size.width = self.bounds.width + self.contentAreaInScreenSpace = updatedContentAreaInScreenSpace + } + } + + if let validLayout = self.validLayout { + self.updateLayout(layout: validLayout, transition: .immediate, previousActionsContainerNode: nil) + } + + if !self.dimNode.isHidden { + self.dimNode.alpha = 1.0 + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) + } else { + self.withoutBlurDimNode.alpha = 1.0 + self.withoutBlurDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) + } + + if #available(iOS 10.0, *) { + if let propertyAnimator = self.propertyAnimator { + let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator + propertyAnimator?.stopAnimation(true) + } + self.effectView.effect = makeCustomZoomBlurEffect(isLight: presentationData.theme.rootController.keyboardColor == .light) + self.effectView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) + self.propertyAnimator = UIViewPropertyAnimator(duration: 0.2 * animationDurationFactor * UIView.animationDurationFactor(), curve: .easeInOut, animations: { + }) + } + + if let _ = self.propertyAnimator { + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] value in + (self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value + }, completion: { [weak self] in + self?.didCompleteAnimationIn = true + self?.hapticFeedback.prepareTap() + self?.actionsContainerNode.animateIn() + }) + } + } else { + UIView.animate(withDuration: 0.2 * animationDurationFactor, animations: { + self.effectView.effect = makeCustomZoomBlurEffect(isLight: self.presentationData.theme.rootController.keyboardColor == .light) + }, completion: { [weak self] _ in + self?.didCompleteAnimationIn = true + self?.actionsContainerNode.animateIn() + }) + } + + if let contentNode = self.contentContainerNode.contentNode { + switch contentNode { + case .reference: + let springDuration: Double = 0.42 * animationDurationFactor + let springDamping: CGFloat = 104.0 + + self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) + self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { + let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) + + let localContentSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.contentContainerNode.view.superview) + + self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y) + self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { [weak self] _ in + self?.animatedIn = true + }) + } + case .extractedContainer: + break + case let .extracted(extracted, keepInPlace): + let springDuration: Double = 0.42 * animationDurationFactor + var springDamping: CGFloat = 104.0 + if case let .extracted(source) = self.legacySource, source.centerVertically { + springDamping = 124.0 + } + + self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) + self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { + let contentParentNode = extracted + let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) + + var actionsDuration = springDuration + var actionsOffset: CGFloat = 0.0 + var contentDuration = springDuration + if case let .extracted(source) = self.legacySource, source.centerVertically { + actionsOffset = -(originalProjectedContentViewFrame.1.height - originalProjectedContentViewFrame.0.height) * 0.57 + actionsDuration *= 1.0 + contentDuration *= 0.9 + } + + let localContentSourceFrame: CGRect + if keepInPlace { + localContentSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.contentContainerNode.view.superview) + } else { + localContentSourceFrame = localSourceFrame + } + + self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y + actionsOffset)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: actionsDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY) + self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: contentDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { [weak self] _ in + self?.clippingNode.view.mask = nil + self?.animatedIn = true + }) + contentParentNode.applyAbsoluteOffsetSpring?(-contentContainerOffset.y, springDuration, springDamping) + } + + extracted.willUpdateIsExtractedToContextPreview?(true, .animated(duration: 0.2, curve: .easeInOut)) + case .controller: + let springDuration: Double = 0.52 * animationDurationFactor + let springDamping: CGFloat = 110.0 + + self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) + self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + self.contentContainerNode.allowsGroupOpacity = true + self.contentContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor, completion: { [weak self] _ in + self?.contentContainerNode.allowsGroupOpacity = false + }) + + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { + let localSourceFrame = self.view.convert(CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalProjectedContentViewFrame.1.minY), size: CGSize(width: originalProjectedContentViewFrame.1.width, height: originalProjectedContentViewFrame.1.height)), to: self.scrollNode.view) + + self.contentContainerNode.layer.animateSpring(from: min(localSourceFrame.width / self.contentContainerNode.frame.width, localSourceFrame.height / self.contentContainerNode.frame.height) as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + + switch self.legacySource { + case let .controller(controller): + controller.animatedIn() + default: + break + } + + let contentContainerOffset = CGPoint(x: localSourceFrame.center.x - self.contentContainerNode.frame.center.x, y: localSourceFrame.center.y - self.contentContainerNode.frame.center.y) + if let contentNode = self.contentContainerNode.contentNode, case let .controller(controller) = contentNode { + let snapshotView: UIView? = nil// controller.sourceNode.view.snapshotContentTree() + if let snapshotView = snapshotView { + controller.sourceView.isHidden = true + + self.view.insertSubview(snapshotView, belowSubview: self.contentContainerNode.view) + snapshotView.layer.animateSpring(from: NSValue(cgPoint: localSourceFrame.center), to: NSValue(cgPoint: CGPoint(x: self.contentContainerNode.frame.midX, y: self.contentContainerNode.frame.minY + localSourceFrame.height / 2.0)), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false) + //snapshotView.layer.animateSpring(from: 1.0 as NSNumber, to: (self.contentContainerNode.frame.width / localSourceFrame.width) as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { [weak self] _ in + self?.animatedIn = true + }) + } + } + } + } + + private var delayLayoutUpdate = false + func animateOut(result initialResult: ContextMenuActionResult, completion: @escaping () -> Void) { + self.isUserInteractionEnabled = false + + self.beganAnimatingOut() + + if let sourceContainer = self.sourceContainer { + sourceContainer.animateOut(result: initialResult, completion: completion) + return + } + + var transitionDuration: Double = 0.2 + var transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut + + var result = initialResult + + switch self.legacySource { + case let .location(source): + let transitionInfo = source.transitionInfo() + if transitionInfo == nil { + result = .dismissWithoutContent + } + + switch result { + case let .custom(value): + switch value { + case let .animated(duration, curve): + transitionDuration = duration + transitionCurve = curve + default: + break + } + default: + break + } + + self.isUserInteractionEnabled = false + self.isAnimatingOut = true + + self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false) + + if !self.dimNode.isHidden { + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) + } else { + self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) + } + + self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15 * animationDurationFactor, removeOnCompletion: false, completion: { _ in + completion() + }) + self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) + + let animateOutToItem: Bool + switch result { + case .default, .custom: + animateOutToItem = true + case .dismissWithoutContent: + animateOutToItem = false + } + + if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { + let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) + self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) + } + case let .reference(source): + guard let maybeContentNode = self.contentContainerNode.contentNode, case let .reference(referenceView) = maybeContentNode else { + return + } + + let transitionInfo = source.transitionInfo() + if transitionInfo == nil { + result = .dismissWithoutContent + } + + switch result { + case let .custom(value): + switch value { + case let .animated(duration, curve): + transitionDuration = duration + transitionCurve = curve + default: + break + } + default: + break + } + + self.isUserInteractionEnabled = false + self.isAnimatingOut = true + + self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false) + + if let transitionInfo = transitionInfo, let parentSuperview = referenceView.superview { + self.originalProjectedContentViewFrame = (convertFrame(referenceView.frame, from: parentSuperview, to: self.view), convertFrame(referenceView.bounds, from: referenceView, to: self.view)) + + var updatedContentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace + updatedContentAreaInScreenSpace.origin.x = 0.0 + updatedContentAreaInScreenSpace.size.width = self.bounds.width + + self.clippingNode.layer.animateFrame(from: self.clippingNode.frame, to: updatedContentAreaInScreenSpace, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) + self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) + } + + if !self.dimNode.isHidden { + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) + } else { + self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) + } + + self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15 * animationDurationFactor, removeOnCompletion: false, completion: { _ in + completion() + }) + self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) + + let animateOutToItem: Bool + switch result { + case .default, .custom: + animateOutToItem = true + case .dismissWithoutContent: + animateOutToItem = false + } + + if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { + let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) + self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) + } + case let .extracted(source): + guard let maybeContentNode = self.contentContainerNode.contentNode, case let .extracted(contentParentNode, keepInPlace) = maybeContentNode else { + return + } + + let putBackInfo = source.putBack() + + if putBackInfo == nil { + result = .dismissWithoutContent + } + + switch result { + case let .custom(value): + switch value { + case let .animated(duration, curve): + transitionDuration = duration + transitionCurve = curve + default: + break + } + default: + break + } + + self.isUserInteractionEnabled = false + self.isAnimatingOut = true + + self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false) + + var completedEffect = false + var completedContentNode = false + var completedActionsNode = false + + if let putBackInfo = putBackInfo, let parentSupernode = contentParentNode.supernode { + self.originalProjectedContentViewFrame = (convertFrame(contentParentNode.frame, from: parentSupernode.view, to: self.view), convertFrame(contentParentNode.contentRect, from: contentParentNode.view, to: self.view)) + + var updatedContentAreaInScreenSpace = putBackInfo.contentAreaInScreenSpace + updatedContentAreaInScreenSpace.origin.x = 0.0 + updatedContentAreaInScreenSpace.size.width = self.bounds.width + + self.clippingNode.view.mask = putBackInfo.maskView + let previousFrame = self.clippingNode.frame + self.clippingNode.position = updatedContentAreaInScreenSpace.center + self.clippingNode.bounds = CGRect(origin: CGPoint(), size: updatedContentAreaInScreenSpace.size) + self.clippingNode.layer.animatePosition(from: previousFrame.center, to: updatedContentAreaInScreenSpace.center, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: true) + self.clippingNode.layer.animateBounds(from: CGRect(origin: CGPoint(), size: previousFrame.size), to: CGRect(origin: CGPoint(), size: updatedContentAreaInScreenSpace.size), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: true) + //self.clippingNode.layer.animateFrame(from: previousFrame, to: updatedContentAreaInScreenSpace, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) + //self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) + } + + let intermediateCompletion: () -> Void = { [weak self, weak contentParentNode] in + if completedEffect && completedContentNode && completedActionsNode { + switch result { + case .default, .custom: + if let contentParentNode = contentParentNode { + contentParentNode.addSubnode(contentParentNode.contentNode) + contentParentNode.isExtractedToContextPreview = false + contentParentNode.isExtractedToContextPreviewUpdated?(false) + } + case .dismissWithoutContent: + break + } + + self?.clippingNode.view.mask = nil + + completion() + } + } + + if #available(iOS 10.0, *) { + if let propertyAnimator = self.propertyAnimator { + let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator + propertyAnimator?.stopAnimation(true) + } + self.propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration * UIView.animationDurationFactor(), curve: .easeInOut, animations: { + //self?.effectView.effect = nil + }) + } + + if let _ = self.propertyAnimator { + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor * UIView.animationDurationFactor(), from: 0.0, to: 0.999, update: { [weak self] value in + (self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value + }, completion: { + completedEffect = true + intermediateCompletion() + }) + } + self.effectView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) + } else { + UIView.animate(withDuration: 0.21 * animationDurationFactor, animations: { + if #available(iOS 9.0, *) { + self.effectView.effect = nil + } else { + self.effectView.alpha = 0.0 + } + }, completion: { _ in + completedEffect = true + intermediateCompletion() + }) + } + + if !self.dimNode.isHidden { + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) + } else { + self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) + } + + self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15 * animationDurationFactor, removeOnCompletion: false, completion: { _ in + completedActionsNode = true + intermediateCompletion() + }) + self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) + + let animateOutToItem: Bool + switch result { + case .default, .custom: + animateOutToItem = true + case .dismissWithoutContent: + animateOutToItem = false + } + + if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { + let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) + let localContentSourceFrame: CGRect + if keepInPlace { + localContentSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.contentContainerNode.view.superview) + } else { + localContentSourceFrame = localSourceFrame + } + + var actionsOffset: CGFloat = 0.0 + if case let .extracted(source) = self.legacySource, source.centerVertically { + actionsOffset = -localSourceFrame.width * 0.6 + } + + self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y + actionsOffset), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) + let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY) + self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentContainerOffset, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true, completion: { _ in + completedContentNode = true + intermediateCompletion() + }) + contentParentNode.updateAbsoluteRect?(self.contentContainerNode.frame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y + contentContainerOffset.y), self.bounds.size) + contentParentNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: -contentContainerOffset.y), transitionCurve, transitionDuration) + + contentParentNode.willUpdateIsExtractedToContextPreview?(false, .animated(duration: 0.2, curve: .easeInOut)) + } else { + if let snapshotView = contentParentNode.contentNode.view.snapshotContentTree(keepTransform: true) { + self.contentContainerNode.view.addSubview(snapshotView) + } + + contentParentNode.addSubnode(contentParentNode.contentNode) + contentParentNode.isExtractedToContextPreview = false + contentParentNode.isExtractedToContextPreviewUpdated?(false) + + self.contentContainerNode.allowsGroupOpacity = true + self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false, completion: { _ in + completedContentNode = true + intermediateCompletion() + }) + + contentParentNode.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + contentParentNode.willUpdateIsExtractedToContextPreview?(false, .animated(duration: 0.2, curve: .easeInOut)) + } + case let .controller(source): + guard let maybeContentNode = self.contentContainerNode.contentNode, case let .controller(controller) = maybeContentNode else { + return + } + + let transitionInfo = source.transitionInfo() + + if transitionInfo == nil { + result = .dismissWithoutContent + } + + switch result { + case let .custom(value): + switch value { + case let .animated(duration, curve): + transitionDuration = duration + transitionCurve = curve + default: + break + } + default: + break + } + + self.isUserInteractionEnabled = false + self.isAnimatingOut = true + + self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false) + + var completedEffect = false + var completedContentNode = false + var completedActionsNode = false + + if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() { + let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view) + self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) + + var updatedContentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace + updatedContentAreaInScreenSpace.origin.x = 0.0 + updatedContentAreaInScreenSpace.size.width = self.bounds.width + } + + let intermediateCompletion: () -> Void = { + if completedEffect && completedContentNode && completedActionsNode { + switch result { + case .default, .custom: + break + case .dismissWithoutContent: + break + } + + completion() + } + } + + if #available(iOS 10.0, *) { + if let propertyAnimator = self.propertyAnimator { + let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator + propertyAnimator?.stopAnimation(true) + } + self.propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration * UIView.animationDurationFactor(), curve: .easeInOut, animations: { [weak self] in + self?.effectView.effect = nil + }) + } + + if let _ = self.propertyAnimator { + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor * UIView.animationDurationFactor(), from: 0.0, to: 0.999, update: { [weak self] value in + (self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value + }, completion: { + completedEffect = true + intermediateCompletion() + }) + } + self.effectView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.05 * animationDurationFactor, delay: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) + } else { + UIView.animate(withDuration: 0.21 * animationDurationFactor, animations: { + if #available(iOS 9.0, *) { + self.effectView.effect = nil + } else { + self.effectView.alpha = 0.0 + } + }, completion: { _ in + completedEffect = true + intermediateCompletion() + }) + } + + if !self.dimNode.isHidden { + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) + } else { + self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) + } + self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false, completion: { _ in + completedActionsNode = true + intermediateCompletion() + }) + self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false, completion: { _ in + }) + self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) + self.contentContainerNode.layer.animateScale(from: 1.0, to: 0.01, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) + + let animateOutToItem: Bool + switch result { + case .default, .custom: + animateOutToItem = true + case .dismissWithoutContent: + animateOutToItem = false + } + + if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { + let localSourceFrame = self.view.convert(CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalProjectedContentViewFrame.1.minY), size: CGSize(width: originalProjectedContentViewFrame.1.width, height: originalProjectedContentViewFrame.1.height)), to: self.scrollNode.view) + + self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) + let contentContainerOffset = CGPoint(x: localSourceFrame.center.x - self.contentContainerNode.frame.center.x, y: localSourceFrame.center.y - self.contentContainerNode.frame.center.y) + self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentContainerOffset, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true, completion: { [weak self] _ in + completedContentNode = true + if let strongSelf = self, let contentNode = strongSelf.contentContainerNode.contentNode, case let .controller(controller) = contentNode { + controller.sourceView.isHidden = false + } + intermediateCompletion() + }) + } else { + if let contentNode = self.contentContainerNode.contentNode, case let .controller(controller) = contentNode { + controller.sourceView.isHidden = false + } + + if let snapshotView = controller.view.snapshotContentTree(keepTransform: true) { + self.contentContainerNode.view.addSubview(snapshotView) + } + + self.contentContainerNode.allowsGroupOpacity = true + self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false, completion: { _ in + completedContentNode = true + intermediateCompletion() + }) + } + } + } + + func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + if let sourceContainer = self.sourceContainer { + sourceContainer.addRelativeContentOffset(offset, transition: transition) + } + } + + func cancelReactionAnimation() { + if let sourceContainer = self.sourceContainer { + sourceContainer.cancelReactionAnimation() + } + } + + func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: @escaping () -> Void) { + if let sourceContainer = self.sourceContainer { + sourceContainer.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, onHit: onHit, completion: completion) + } + } + + func animateDismissalIfNeeded() { + guard let layout = self.validLayout, layout.metrics.isTablet else { + return + } + if let sourceContainer = self.sourceContainer { + sourceContainer.animateOut(result: .dismissWithoutContent, completion: {}) + return + } + } + + func getActionsMinHeight() -> ContextController.ActionsHeight? { + if !self.actionsContainerNode.bounds.height.isZero { + return ContextController.ActionsHeight( + minY: self.actionsContainerNode.frame.minY, + contentOffset: self.scrollNode.view.contentOffset.y + ) + } else { + return nil + } + } + + func setItemsSignal(items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition, animated: Bool) { + if let sourceContainer = self.sourceContainer { + sourceContainer.setItems(items: items, animated: animated) + } else { + self.legacyItems = items + self.itemsDisposable.set((items + |> deliverOnMainQueue).start(next: { [weak self] items in + guard let strongSelf = self else { + return + } + strongSelf.setItems(items: items, minHeight: minHeight, previousActionsTransition: previousActionsTransition) + })) + } + } + + private func setItems(items: ContextController.Items, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { + if let sourceContainer = self.sourceContainer { + let disableAnimations = self.getController()?.immediateItemsTransitionAnimation == true + sourceContainer.setItems(items: .single(items), animated: !disableAnimations) + + if !self.didSetItemsReady { + self.didSetItemsReady = true + self.itemsReady.set(.single(true)) + } + return + } + + if let _ = self.currentItems, !self.didCompleteAnimationIn && self.getController()?.immediateItemsTransitionAnimation == true { + return + } + + self.currentItems = items + self.currentActionsMinHeight = minHeight + + let previousActionsContainerNode = self.actionsContainerNode + let previousActionsContainerFrame = previousActionsContainerNode.view.convert(previousActionsContainerNode.bounds, to: self.view) + self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: items, getController: { [weak self] in + return self?.getController() + }, actionSelected: { [weak self] result in + self?.beginDismiss(result) + }, requestLayout: { [weak self] in + self?.updateLayout() + }, feedbackTap: { [weak self] in + self?.hapticFeedback.tap() + }, blurBackground: self.blurBackground) + self.scrollNode.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode) + + if let layout = self.validLayout { + self.updateLayout(layout: layout, transition: self.didSetItemsReady ? .animated(duration: 0.3, curve: .spring) : .immediate, previousActionsContainerNode: previousActionsContainerNode, previousActionsContainerFrame: previousActionsContainerFrame, previousActionsTransition: previousActionsTransition) + } else { + previousActionsContainerNode.removeFromSupernode() + } + + if !self.didSetItemsReady { + self.didSetItemsReady = true + self.itemsReady.set(.single(true)) + } + } + + func pushItems(items: Signal) { + if let sourceContainer = self.sourceContainer { + sourceContainer.pushItems(items: items) + } + } + + func popItems() { + if let sourceContainer = self.sourceContainer { + sourceContainer.popItems() + } + } + + func updateTheme(presentationData: PresentationData) { + self.presentationData = presentationData + + self.dimNode.backgroundColor = presentationData.theme.contextMenu.dimColor + self.actionsContainerNode.updateTheme(presentationData: presentationData) + + if let validLayout = self.validLayout { + self.updateLayout(layout: validLayout, transition: .immediate, previousActionsContainerNode: nil, previousActionsContainerFrame: nil) + } + } + + func updateLayout() { + if let layout = self.validLayout { + self.updateLayout(layout: layout, transition: .immediate, previousActionsContainerNode: nil) + } + } + + func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?, previousActionsContainerFrame: CGRect? = nil, previousActionsTransition: ContextController.PreviousActionsTransition = .scale) { + if self.isAnimatingOut || self.delayLayoutUpdate { + return + } + + self.validLayout = layout + + if let sourceContainer = self.sourceContainer { + transition.updateFrame(node: sourceContainer, frame: CGRect(origin: CGPoint(), size: layout.size)) + sourceContainer.update( + presentationData: self.presentationData, + layout: layout, + transition: transition + ) + return + } + + var actionsContainerTransition = transition + if previousActionsContainerNode != nil { + actionsContainerTransition = .immediate + } + + transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.withoutBlurDimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + switch layout.metrics.widthClass { + case .compact: + if case .reference = self.legacySource { + } else if case let .extracted(extractedSource) = self.legacySource, !extractedSource.blurBackground { + } else if self.effectView.superview == nil { + self.view.insertSubview(self.effectView, at: 0) + if #available(iOS 10.0, *) { + if let propertyAnimator = self.propertyAnimator { + let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator + propertyAnimator?.stopAnimation(true) + } + } + self.effectView.effect = makeCustomZoomBlurEffect(isLight: presentationData.theme.rootController.keyboardColor == .light) + self.dimNode.alpha = 1.0 + } + self.dimNode.isHidden = false + self.withoutBlurDimNode.isHidden = true + case .regular: + if case .reference = self.legacySource { + } else if case let .extracted(extractedSource) = self.legacySource, !extractedSource.blurBackground { + } else if self.effectView.superview != nil { + self.effectView.removeFromSuperview() + self.withoutBlurDimNode.alpha = 1.0 + } + self.dimNode.isHidden = true + self.withoutBlurDimNode.isHidden = false + } + transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let actionsSideInset: CGFloat = layout.safeInsets.left + 12.0 + let contentTopInset: CGFloat = max(11.0, layout.statusBarHeight ?? 0.0) + + let actionsBottomInset: CGFloat = 11.0 + + if let contentNode = self.contentContainerNode.contentNode { + switch contentNode { + case let .reference(referenceNode): + let contentActionsSpacing: CGFloat = 8.0 + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { + let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero + let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) + + let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, presentation: .inline, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: layout.size.height, transition: actionsContainerTransition) + let adjustedActionsSize = realActionsSize + + self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize) + let contentSize = originalProjectedContentViewFrame.1.size + self.contentContainerNode.updateLayout(size: contentSize, scaledSize: contentSize, transition: transition) + + let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - adjustedActionsSize.height) + + let originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin) + let preferredActionsX = originalProjectedContentViewFrame.1.minX + + var originalActionsFrame = CGRect(origin: CGPoint(x: max(actionsSideInset, min(layout.size.width - adjustedActionsSize.width - actionsSideInset, preferredActionsX)), y: originalActionsY), size: realActionsSize) + let originalContentX: CGFloat = originalProjectedContentViewFrame.1.minX + let originalContentY = originalProjectedContentViewFrame.1.minY + + var originalContentFrame = CGRect(origin: CGPoint(x: originalContentX, y: originalContentY), size: originalProjectedContentViewFrame.1.size) + let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0) + let bottomEdge = min(layout.size.height - layout.intrinsicInsets.bottom, self.contentAreaInScreenSpace?.maxY ?? layout.size.height) + + if originalContentFrame.minY < topEdge { + let requiredOffset = topEdge - originalContentFrame.minY + let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY) + let offset = min(requiredOffset, availableOffset) + originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) + originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) + } else if originalActionsFrame.maxY > bottomEdge { + let requiredOffset = bottomEdge - originalActionsFrame.maxY + let offset = requiredOffset + originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) + originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) + } + + var contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset) + contentHeight = max(contentHeight, adjustedActionsSize.height + originalActionsFrame.minY + actionsBottomInset) + + var overflowOffset: CGFloat + var contentContainerFrame: CGRect + + overflowOffset = min(0.0, originalActionsFrame.minY - contentTopInset) + let contentParentNode = referenceNode + contentContainerFrame = originalContentFrame + if !overflowOffset.isZero { + let offsetDelta = contentParentNode.frame.height + 4.0 + overflowOffset += offsetDelta + overflowOffset = min(0.0, overflowOffset) + + originalActionsFrame.origin.x -= contentParentNode.frame.width + 14.0 + originalActionsFrame.origin.x = max(actionsSideInset, originalActionsFrame.origin.x) + + if originalActionsFrame.minX < contentContainerFrame.minX { + contentContainerFrame.origin.x = min(originalActionsFrame.maxX + 14.0, layout.size.width - actionsSideInset) + } + originalActionsFrame.origin.y += offsetDelta + if originalActionsFrame.maxY < originalContentFrame.maxY { + originalActionsFrame.origin.y += contentParentNode.frame.height + originalActionsFrame.origin.y = min(originalActionsFrame.origin.y, layout.size.height - originalActionsFrame.height - actionsBottomInset) + } + contentHeight -= offsetDelta + } + + if let customPosition = self.customPosition { + originalActionsFrame.origin.x = floor(originalContentFrame.center.x - originalActionsFrame.width / 2.0) + customPosition.x + originalActionsFrame.origin.y = floor(originalContentFrame.center.y - originalActionsFrame.height / 2.0) + customPosition.y + } + + let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) + if self.scrollNode.view.contentSize != scrollContentSize { + self.scrollNode.view.contentSize = scrollContentSize + } + self.actionsContainerNode.panSelectionGestureEnabled = scrollContentSize.height <= layout.size.height + + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) + + if isInitialLayout { + let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) + if overflowOffset < 0.0 { + transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY) + } + } + } + case .extractedContainer: + break + case let .extracted(contentParentNode, keepInPlace): + var centerVertically = false + if case let .extracted(source) = self.legacySource, source.centerVertically { + centerVertically = true + } + let contentActionsSpacing: CGFloat = keepInPlace ? 16.0 : 8.0 + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { + let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero + let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) + + let constrainedActionsHeight: CGFloat + let constrainedActionsBottomInset: CGFloat + if let currentActionsMinHeight = self.currentActionsMinHeight { + constrainedActionsBottomInset = actionsBottomInset + layout.intrinsicInsets.bottom + constrainedActionsHeight = layout.size.height - currentActionsMinHeight.minY - constrainedActionsBottomInset + } else { + constrainedActionsHeight = layout.size.height + constrainedActionsBottomInset = 0.0 + } + + let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, presentation: .inline, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: constrainedActionsHeight, transition: actionsContainerTransition) + let adjustedActionsSize = realActionsSize + + self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize) + let contentSize = originalProjectedContentViewFrame.1.size + self.contentContainerNode.updateLayout(size: contentSize, scaledSize: contentSize, transition: transition) + + let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - adjustedActionsSize.height) + let preferredActionsX: CGFloat + var originalActionsY: CGFloat + if centerVertically { + originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin) + preferredActionsX = originalProjectedContentViewFrame.1.maxX - adjustedActionsSize.width + } else if keepInPlace { + originalActionsY = originalProjectedContentViewFrame.1.minY - contentActionsSpacing - adjustedActionsSize.height + preferredActionsX = max(actionsSideInset, originalProjectedContentViewFrame.1.maxX - adjustedActionsSize.width) + } else { + originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin) + preferredActionsX = originalProjectedContentViewFrame.1.minX + } + + if let currentActionsMinHeight = self.currentActionsMinHeight { + originalActionsY = currentActionsMinHeight.minY + } + + var originalActionsFrame = CGRect(origin: CGPoint(x: max(actionsSideInset, min(layout.size.width - adjustedActionsSize.width - actionsSideInset, preferredActionsX)), y: originalActionsY), size: realActionsSize) + let originalContentX: CGFloat = originalProjectedContentViewFrame.1.minX + let originalContentY: CGFloat + if keepInPlace { + originalContentY = originalProjectedContentViewFrame.1.minY + } else { + originalContentY = originalActionsFrame.minY - contentActionsSpacing - originalProjectedContentViewFrame.1.size.height + } + var originalContentFrame = CGRect(origin: CGPoint(x: originalContentX, y: originalContentY), size: originalProjectedContentViewFrame.1.size) + let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0) + if originalContentFrame.minY < topEdge { + let requiredOffset = topEdge - originalContentFrame.minY + let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY) + let offset = min(requiredOffset, availableOffset) + originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) + originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) + } + + var contentHeight: CGFloat + if keepInPlace { + contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalActionsFrame.minY + contentTopInset) + } else { + if self.currentActionsMinHeight != nil { + contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset + layout.intrinsicInsets.bottom)) + } else { + contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset + layout.intrinsicInsets.bottom) - originalContentFrame.minY + contentTopInset) + } + } + + var overflowOffset: CGFloat + var contentContainerFrame: CGRect + if centerVertically { + overflowOffset = 0.0 + if layout.size.width > layout.size.height, case .compact = layout.metrics.widthClass { + let totalWidth = originalContentFrame.width + originalActionsFrame.width + contentActionsSpacing + contentContainerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - totalWidth) / 2.0 + originalContentFrame.width * 0.1), y: floor((layout.size.height - originalContentFrame.height) / 2.0)), size: originalContentFrame.size) + originalActionsFrame.origin.x = contentContainerFrame.maxX + contentActionsSpacing + 14.0 + originalActionsFrame.origin.y = contentContainerFrame.origin.y + contentHeight = layout.size.height + } else { + let totalHeight = originalContentFrame.height + originalActionsFrame.height + contentContainerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - originalContentFrame.width) / 2.0), y: floor((layout.size.height - totalHeight) / 2.0)), size: originalContentFrame.size) + originalActionsFrame.origin.y = contentContainerFrame.maxY + contentActionsSpacing + } + } else if keepInPlace { + overflowOffset = min(0.0, originalActionsFrame.minY - contentTopInset) + contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -contentParentNode.contentRect.minY) + if !overflowOffset.isZero { + let offsetDelta = contentParentNode.contentRect.height + 4.0 + overflowOffset += offsetDelta + overflowOffset = min(0.0, overflowOffset) + + originalActionsFrame.origin.x -= contentParentNode.contentRect.maxX - contentParentNode.contentRect.minX + 14.0 + originalActionsFrame.origin.x = max(actionsSideInset, originalActionsFrame.origin.x) + //originalActionsFrame.origin.y += contentParentNode.contentRect.height + if originalActionsFrame.minX < contentContainerFrame.minX { + contentContainerFrame.origin.x = min(originalActionsFrame.maxX + 14.0, layout.size.width - actionsSideInset) + } + originalActionsFrame.origin.y += offsetDelta + if originalActionsFrame.maxY < originalContentFrame.maxY { + originalActionsFrame.origin.y += contentParentNode.contentRect.height + originalActionsFrame.origin.y = min(originalActionsFrame.origin.y, layout.size.height - originalActionsFrame.height - actionsBottomInset) + } + contentHeight -= offsetDelta + } + } else { + overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) + contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -overflowOffset - contentParentNode.contentRect.minY) + + if contentContainerFrame.maxX > layout.size.width { + contentContainerFrame = CGRect(origin: CGPoint(x: layout.size.width - contentContainerFrame.width - 11.0, y: contentContainerFrame.minY), size: contentContainerFrame.size) + } + } + + let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) + if self.scrollNode.view.contentSize != scrollContentSize { + self.scrollNode.view.contentSize = scrollContentSize + } + self.actionsContainerNode.panSelectionGestureEnabled = scrollContentSize.height <= layout.size.height + + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) + + if isInitialLayout { + //let previousContentOffset = self.scrollNode.view.contentOffset.y + if !keepInPlace { + if let currentActionsMinHeight = self.currentActionsMinHeight { + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: currentActionsMinHeight.contentOffset) + } else { + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset) + } + } + let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) + var offset: CGFloat = 0.0 + //offset -= previousContentOffset - self.scrollNode.view.contentOffset.y + offset += previousContainerFrame.minY - currentContainerFrame.minY + transition.animatePositionAdditive(node: self.contentContainerNode, offset: CGPoint(x: 0.0, y: offset)) + if overflowOffset < 0.0 { + let _ = currentContainerFrame + let _ = previousContainerFrame + } + } + + let absoluteContentRect = contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y) + + contentParentNode.updateAbsoluteRect?(absoluteContentRect, layout.size) + } + case let .controller(contentParentNode): + var projectedFrame: CGRect = convertFrame(contentParentNode.sourceView.bounds, from: contentParentNode.sourceView, to: self.view) + switch self.legacySource { + case let .controller(source): + let transitionInfo = source.transitionInfo() + if let (sourceView, sourceRect) = transitionInfo?.sourceNode() { + projectedFrame = convertFrame(sourceRect, from: sourceView, to: self.view) + } + default: + break + } + self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) + + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { + let contentActionsSpacing: CGFloat = actionsSideInset + let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0) + + let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero + let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) + + let constrainedWidth: CGFloat + if layout.size.width < layout.size.height { + constrainedWidth = layout.size.width + } else { + constrainedWidth = floor(layout.size.width / 2.0) + } + + let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, presentation: .inline, constrainedWidth: constrainedWidth - actionsSideInset * 2.0, constrainedHeight: layout.size.height, transition: actionsContainerTransition) + let contentScale = (constrainedWidth - actionsSideInset * 2.0) / constrainedWidth + var contentUnscaledSize: CGSize + if case .compact = layout.metrics.widthClass { + self.actionsContainerNode.updateSize(containerSize: actionsSize, contentSize: actionsSize) + + let proposedContentHeight: CGFloat + if layout.size.width < layout.size.height { + proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset + } else { + proposedContentHeight = layout.size.height - topEdge - topEdge + + let maxActionsHeight = layout.size.height - topEdge - topEdge + self.actionsContainerNode.updateSize(containerSize: CGSize(width: actionsSize.width, height: min(actionsSize.height, maxActionsHeight)), contentSize: actionsSize) + } + contentUnscaledSize = CGSize(width: constrainedWidth, height: max(100.0, proposedContentHeight)) + + if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { + contentUnscaledSize = preferredSize + } + } else { + let maxActionsHeight = layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height + self.actionsContainerNode.updateSize(containerSize: CGSize(width: actionsSize.width, height: min(actionsSize.height, maxActionsHeight)), contentSize: actionsSize) + + let proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset + contentUnscaledSize = CGSize(width: min(layout.size.width, 340.0), height: min(568.0, proposedContentHeight)) + + if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { + contentUnscaledSize = preferredSize + } + } + let contentSize = CGSize(width: floor(contentUnscaledSize.width * contentScale), height: floor(contentUnscaledSize.height * contentScale)) + + self.contentContainerNode.updateLayout(size: contentUnscaledSize, scaledSize: contentSize, transition: transition) + + let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height) + var originalActionsFrame: CGRect + var originalContentFrame: CGRect + var contentHeight: CGFloat + if case .compact = layout.metrics.widthClass { + if layout.size.width < layout.size.height { + let sideInset = floor((layout.size.width - max(contentSize.width, actionsSize.width)) / 2.0) + originalActionsFrame = CGRect(origin: CGPoint(x: sideInset, y: min(maximumActionsFrameOrigin, floor((layout.size.height - contentActionsSpacing - contentSize.height) / 2.0) + contentSize.height + contentActionsSpacing)), size: actionsSize) + originalContentFrame = CGRect(origin: CGPoint(x: sideInset, y: originalActionsFrame.minY - contentActionsSpacing - contentSize.height), size: contentSize) + if originalContentFrame.minY < topEdge { + let requiredOffset = topEdge - originalContentFrame.minY + let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY) + let offset = min(requiredOffset, availableOffset) + originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) + originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) + } + contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset) + } else { + originalContentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width - actionsSideInset - actionsSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) + originalActionsFrame = CGRect(origin: CGPoint(x: originalContentFrame.maxX + actionsSideInset, y: max(topEdge, originalContentFrame.minY)), size: actionsSize) + contentHeight = max(layout.size.height, max(originalContentFrame.maxY, originalActionsFrame.maxY)) + } + } else { + originalContentFrame = CGRect(origin: CGPoint(x: floor(originalProjectedContentViewFrame.1.midX - contentSize.width / 2.0), y: floor(originalProjectedContentViewFrame.1.midY - contentSize.height / 2.0)), size: contentSize) + originalContentFrame.origin.x = min(originalContentFrame.origin.x, layout.size.width - actionsSideInset - contentSize.width) + originalContentFrame.origin.x = max(originalContentFrame.origin.x, actionsSideInset) + originalContentFrame.origin.y = min(originalContentFrame.origin.y, layout.size.height - layout.intrinsicInsets.bottom - actionsSideInset - contentSize.height) + originalContentFrame.origin.y = max(originalContentFrame.origin.y, contentTopInset) + if originalContentFrame.maxX <= layout.size.width - actionsSideInset - actionsSize.width - contentActionsSpacing { + originalActionsFrame = CGRect(origin: CGPoint(x: originalContentFrame.maxX + contentActionsSpacing, y: originalContentFrame.minY), size: actionsSize) + if originalActionsFrame.maxX > layout.size.width - actionsSideInset { + let offset = originalActionsFrame.maxX - (layout.size.width - actionsSideInset) + originalActionsFrame.origin.x -= offset + originalContentFrame.origin.x -= offset + } + } else { + originalActionsFrame = CGRect(origin: CGPoint(x: originalContentFrame.minX - contentActionsSpacing - actionsSize.width, y: originalContentFrame.minY), size: actionsSize) + if originalActionsFrame.minX < actionsSideInset { + let offset = actionsSideInset - originalActionsFrame.minX + originalActionsFrame.origin.x += offset + originalContentFrame.origin.x += offset + } + } + contentHeight = layout.size.height + contentHeight = max(contentHeight, originalActionsFrame.maxY + actionsBottomInset) + contentHeight = max(contentHeight, originalContentFrame.maxY + actionsBottomInset) + } + + let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) + if self.scrollNode.view.contentSize != scrollContentSize { + self.scrollNode.view.contentSize = scrollContentSize + } + self.actionsContainerNode.panSelectionGestureEnabled = true + + let overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) + + let contentContainerFrame = originalContentFrame + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) + actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) + + if isInitialLayout { + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset) + let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) + if overflowOffset < 0.0 { + transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY) + } + } + } + } + } + + if let previousActionsContainerNode = previousActionsContainerNode { + if transition.isAnimated && self.getController()?.immediateItemsTransitionAnimation == false { + if previousActionsContainerNode.hasAdditionalActions && !self.actionsContainerNode.hasAdditionalActions && self.getController()?.useComplexItemsTransitionAnimation == true { + var initialFrame = self.actionsContainerNode.frame + let delta = (previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height) + initialFrame.origin.y = self.actionsContainerNode.frame.minY + previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height + transition.animateFrame(node: self.actionsContainerNode, from: initialFrame) + transition.animatePosition(node: previousActionsContainerNode, to: CGPoint(x: 0.0, y: -delta), removeOnCompletion: false, additive: true) + previousActionsContainerNode.animateOut(offset: delta, transition: transition) + + previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in + previousActionsContainerNode?.removeFromSupernode() + }) + self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } else { + if let previousActionsContainerFrame = previousActionsContainerFrame { + previousActionsContainerNode.frame = self.view.convert(previousActionsContainerFrame, to: self.actionsContainerNode.view.superview!) + } + + switch previousActionsTransition { + case .scale: + transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.1) + previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in + previousActionsContainerNode?.removeFromSupernode() + }) + + transition.animateTransformScale(node: self.actionsContainerNode, from: 0.1) + self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + case let .slide(forward): + let deltaY = self.actionsContainerNode.frame.minY - previousActionsContainerNode.frame.minY + var previousNodePosition = previousActionsContainerNode.position.offsetBy(dx: 0.0, dy: deltaY) + let additionalHorizontalOffset: CGFloat = 20.0 + let currentNodeOffset: CGFloat + if forward { + previousNodePosition = previousNodePosition.offsetBy(dx: -previousActionsContainerNode.frame.width / 2.0 - additionalHorizontalOffset, dy: -previousActionsContainerNode.frame.height / 2.0) + currentNodeOffset = self.actionsContainerNode.bounds.width / 2.0 + additionalHorizontalOffset + } else { + previousNodePosition = previousNodePosition.offsetBy(dx: previousActionsContainerNode.frame.width / 2.0 + additionalHorizontalOffset, dy: -previousActionsContainerNode.frame.height / 2.0) + currentNodeOffset = -self.actionsContainerNode.bounds.width / 2.0 - additionalHorizontalOffset + } + transition.updatePosition(node: previousActionsContainerNode, position: previousNodePosition) + transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.01) + previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in + previousActionsContainerNode?.removeFromSupernode() + }) + + transition.animatePositionAdditive(node: self.actionsContainerNode, offset: CGPoint(x: currentNodeOffset, y: -deltaY - self.actionsContainerNode.bounds.height / 2.0)) + transition.animateTransformScale(node: self.actionsContainerNode, from: 0.01) + self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + } else { + previousActionsContainerNode.removeFromSupernode() + } + } + + transition.updateFrame(node: self.dismissNode, frame: CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize)) + self.dismissAccessibilityArea.frame = CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize) + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard let layout = self.validLayout else { + return + } + if let maybeContentNode = self.contentContainerNode.contentNode, case let .extracted(contentParentNode, keepInPlace) = maybeContentNode { + let contentContainerFrame = self.contentContainerNode.frame + let absoluteRect: CGRect + if keepInPlace { + absoluteRect = contentContainerFrame + } else { + absoluteRect = contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y) + } + contentParentNode.updateAbsoluteRect?(absoluteRect, layout.size) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if !self.isUserInteractionEnabled { + return nil + } + + if let controller = self.getController() as? ContextController { + var innerResult: UIView? + controller.forEachController { c in + if let c = c as? UndoOverlayController { + if let result = c.view.hitTest(self.view.convert(point, to: c.view), with: event) { + innerResult = result + return false + } + } + return true + } + if let innerResult = innerResult { + return innerResult + } + } + + if let sourceContainer = self.sourceContainer { + return sourceContainer.hitTest(self.view.convert(point, to: sourceContainer.view), with: event) + } + + let mappedPoint = self.view.convert(point, to: self.scrollNode.view) + var maybePassthrough: ContextControllerImpl.HandledTouchEvent? + if let maybeContentNode = self.contentContainerNode.contentNode { + switch maybeContentNode { + case .reference: + if let controller = self.getController() as? ContextControllerImpl, let passthroughTouchEvent = controller.passthroughTouchEvent { + maybePassthrough = passthroughTouchEvent(self.view, point) + } + case let .extracted(contentParentNode, _): + if case let .extracted(source) = self.legacySource { + if !source.ignoreContentTouches { + let contentPoint = self.view.convert(point, to: contentParentNode.contentNode.view) + if let result = contentParentNode.contentNode.customHitTest?(contentPoint) { + return result + } else if let result = contentParentNode.contentNode.hitTest(contentPoint, with: event) { + if result is TextSelectionNodeView { + return result + } else if contentParentNode.contentRect.contains(contentPoint) { + return contentParentNode.contentNode.view + } + } + } + } + case .extractedContainer: + break + case let .controller(controller): + var passthrough = false + switch self.legacySource { + case let .controller(controllerSource): + passthrough = controllerSource.passthroughTouches + default: + break + } + if passthrough { + let controllerPoint = self.view.convert(point, to: controller.controller.view) + if let result = controller.controller.view.hitTest(controllerPoint, with: event) { + return result + } + } + } + } + + if self.actionsContainerNode.frame.contains(mappedPoint) { + return self.actionsContainerNode.hitTest(self.view.convert(point, to: self.actionsContainerNode.view), with: event) + } + + if let maybePassthrough = maybePassthrough { + switch maybePassthrough { + case .ignore: + break + case let .dismiss(consume, hitTestResult): + self.getController()?.dismiss(completion: nil) + + if let hitTestResult = hitTestResult { + return hitTestResult + } + if !consume { + return nil + } + } + } + + return self.dismissNode.view + } + + fileprivate func performHighlightedAction() { + self.sourceContainer?.performHighlightedAction() + } + + fileprivate func decreaseHighlightedIndex() { + self.sourceContainer?.decreaseHighlightedIndex() + } + + fileprivate func increaseHighlightedIndex() { + self.sourceContainer?.increaseHighlightedIndex() + } +} + +public final class ContextControllerImpl: ViewController, ContextController, StandalonePresentableController, ContextControllerProtocol, KeyShortcutResponder { + private let context: AccountContext? + private var presentationData: PresentationData + private let configuration: ContextController.Configuration + + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + private weak var recognizer: TapLongTapOrDoubleTapGestureRecognizer? + private weak var gesture: ContextGesture? + + private var animatedDidAppear = false + private var wasDismissed = false + private var dismissOnInputClose: (result: ContextMenuActionResult, completion: (() -> Void)?)? + private var dismissToReactionOnInputClose: (value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: (() -> Void)?)? + + override public var overlayWantsToBeBelowKeyboard: Bool { + if self.isNodeLoaded { + return self.controllerNode.overlayWantsToBeBelowKeyboard + } else { + return false + } + } + + var controllerNode: ContextControllerNode { + return self.displayNode as! ContextControllerNode + } + + public var dismissed: (() -> Void)? + public var dismissedForCancel: (() -> Void)? { + didSet { + self.controllerNode.dismissedForCancel = self.dismissedForCancel + } + } + + public var useComplexItemsTransitionAnimation = false + public var immediateItemsTransitionAnimation = false + let workaroundUseLegacyImplementation: Bool + let disableScreenshots: Bool + let hideReactionPanelTail: Bool + + public var passthroughTouchEvent: ((UIView, CGPoint) -> HandledTouchEvent)? + + private var shouldBeDismissedDisposable: Disposable? + + public var reactionSelected: ((UpdateMessageReaction, Bool) -> Void)? + public var premiumReactionsSelected: (() -> Void)? + + public var getOverlayViews: (() -> [UIView])? + + public init( + context: AccountContext? = nil, + presentationData: PresentationData, + configuration: ContextController.Configuration, + recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, + gesture: ContextGesture? = nil, + workaroundUseLegacyImplementation: Bool = false, + disableScreenshots: Bool = false, + hideReactionPanelTail: Bool = false + ) { + self.context = context + self.presentationData = presentationData + self.configuration = configuration + self.recognizer = recognizer + self.gesture = gesture + self.workaroundUseLegacyImplementation = workaroundUseLegacyImplementation + self.disableScreenshots = disableScreenshots + self.hideReactionPanelTail = hideReactionPanelTail + + super.init(navigationBarPresentationData: nil) + + if let mainSource = configuration.sources.first(where: { $0.id == configuration.initialId }) { + switch mainSource.source { + case let .location(locationSource): + self.statusBar.statusBarStyle = .Ignore + + self.shouldBeDismissedDisposable = (locationSource.shouldBeDismissed + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.dismiss(result: .default, completion: {}) + }).strict() + case let .reference(referenceSource): + self.statusBar.statusBarStyle = .Ignore + + self.shouldBeDismissedDisposable = (referenceSource.shouldBeDismissed + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.dismiss(result: .default, completion: {}) + }).strict() + case let .extracted(extractedSource): + if extractedSource.blurBackground { + self.statusBar.statusBarStyle = .Hide + } else { + self.statusBar.statusBarStyle = .Ignore + } + self.shouldBeDismissedDisposable = (extractedSource.shouldBeDismissed + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.dismiss(result: .default, completion: {}) + }).strict() + case .controller: + self.statusBar.statusBarStyle = .Hide + } + } + + self.lockOrientation = true + self.blocksBackgroundWhenInOverlay = true + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.shouldBeDismissedDisposable?.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = ContextControllerNode(controller: self, context: self.context, presentationData: self.presentationData, configuration: self.configuration, beginDismiss: { [weak self] result in + self?.dismiss(result: result, completion: nil) + }, recognizer: self.recognizer, gesture: self.gesture, beganAnimatingOut: { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.statusBar.statusBarStyle = .Ignore + + strongSelf.forEachController { c in + if let c = c as? UndoOverlayController { + c.dismiss() + } + return true + } + }, attemptTransitionControllerIntoNavigation: { + }) + self.controllerNode.dismissedForCancel = self.dismissedForCancel + self.displayNodeDidLoad() + + self._ready.set(combineLatest(queue: .mainQueue(), self.controllerNode.itemsReady.get(), self.controllerNode.contentReady.get()) + |> map { values in + return values.0 && values.1 + }) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.updateLayout(layout: layout, transition: transition, previousActionsContainerNode: nil) + + if (layout.inputHeight ?? 0.0) == 0.0 { + if let dismissOnInputClose = self.dismissOnInputClose { + self.dismissOnInputClose = nil + DispatchQueue.main.async { + self.dismiss(result: dismissOnInputClose.result, completion: dismissOnInputClose.completion) + } + } else if let args = self.dismissToReactionOnInputClose { + self.dismissToReactionOnInputClose = nil + DispatchQueue.main.async { + self.dismissWithReactionImpl(value: args.value, targetView: args.targetView, hideNode: args.hideNode, animateTargetContainer: args.animateTargetContainer, addStandaloneReactionAnimation: args.addStandaloneReactionAnimation, reducedCurve: true, onHit: nil, completion: args.completion) + } + } + } + } + + override public func viewDidAppear(_ animated: Bool) { + if self.ignoreAppearanceMethodInvocations() { + return + } + super.viewDidAppear(animated) + + if !self.wasDismissed && !self.animatedDidAppear { + self.animatedDidAppear = true + self.controllerNode.animateIn() + } + } + + public func getActionsMinHeight() -> ContextController.ActionsHeight? { + if self.isNodeLoaded { + return self.controllerNode.getActionsMinHeight() + } + return nil + } + + public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, animated: Bool) { + //self.items = items + + if self.isNodeLoaded { + self.immediateItemsTransitionAnimation = false + self.controllerNode.setItemsSignal(items: items, minHeight: minHeight, previousActionsTransition: .scale, animated: animated) + } else { + assertionFailure() + } + } + + public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { + //self.items = items + + if self.isNodeLoaded { + self.controllerNode.setItemsSignal(items: items, minHeight: minHeight, previousActionsTransition: previousActionsTransition, animated: true) + } else { + assertionFailure() + } + } + + public func pushItems(items: Signal) { + if !self.isNodeLoaded { + return + } + self.controllerNode.pushItems(items: items) + } + + public func popItems() { + if !self.isNodeLoaded { + return + } + self.controllerNode.popItems() + } + + public func updateTheme(presentationData: PresentationData) { + self.presentationData = presentationData + if self.isNodeLoaded { + self.controllerNode.updateTheme(presentationData: presentationData) + } + } + + public func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) { + if let mainSource = self.configuration.sources.first(where: { $0.id == self.configuration.initialId }), case let .reference(source) = mainSource.source, source.forceDisplayBelowKeyboard { + + } else if viewTreeContainsFirstResponder(view: self.view) { + self.dismissOnInputClose = (result, completion) + self.view.endEditing(true) + return + } + + if !self.wasDismissed { + self.wasDismissed = true + + self.controllerNode.animateOut(result: result, completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + completion?() + }) + self.dismissed?() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.dismiss(result: .default, completion: completion) + } + + public func dismissWithCustomTransition(transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) { + self.dismiss(result: .custom(transition), completion: nil) + } + + public func dismissWithoutContent() { + self.dismiss(result: .dismissWithoutContent, completion: nil) + } + + public func dismissNow() { + self.presentingViewController?.dismiss(animated: false, completion: nil) + self.dismissed?() + } + + public func dismissWithReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, onHit: (() -> Void)?, completion: (() -> Void)?) { + self.dismissWithReactionImpl(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: false, onHit: onHit, completion: completion) + } + + private func dismissWithReactionImpl(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: (() -> Void)?) { + if viewTreeContainsFirstResponder(view: self.view) { + self.dismissToReactionOnInputClose = (value, targetView, hideNode, animateTargetContainer, addStandaloneReactionAnimation, completion) + self.view.endEditing(true) + return + } + + if !self.wasDismissed { + self.wasDismissed = true + self.controllerNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, onHit: onHit, completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + completion?() + }) + self.dismissed?() + } + } + + public func animateDismissalIfNeeded() { + self.controllerNode.animateDismissalIfNeeded() + } + + public func cancelReactionAnimation() { + self.controllerNode.cancelReactionAnimation() + } + + public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + self.controllerNode.addRelativeContentOffset(offset, transition: transition) + } + + public var keyShortcuts: [KeyShortcut] { + return [ + KeyShortcut( + input: UIKeyCommand.inputEscape, + modifiers: [], + action: { [weak self] in + self?.dismissWithoutContent() + } + ), + KeyShortcut( + input: "W", + modifiers: [.command], + action: { [weak self] in + self?.dismissWithoutContent() + } + ), + KeyShortcut( + input: "\r", + modifiers: [], + action: { [weak self] in + self?.controllerNode.performHighlightedAction() + } + ), + KeyShortcut( + input: UIKeyCommand.inputUpArrow, + modifiers: [], + action: { [weak self] in + self?.controllerNode.decreaseHighlightedIndex() + } + ), + KeyShortcut( + input: UIKeyCommand.inputDownArrow, + modifiers: [], + action: { [weak self] in + self?.controllerNode.increaseHighlightedIndex() + } + ) + ] + } +} diff --git a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerPresentationNode.swift similarity index 98% rename from submodules/ContextUI/Sources/ContextControllerPresentationNode.swift rename to submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerPresentationNode.swift index 01938595e2..a216c40895 100644 --- a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerPresentationNode.swift @@ -7,6 +7,7 @@ import TextSelectionNode import TelegramCore import SwiftSignalKit import ReactionSelectionNode +import ContextUI enum ContextControllerPresentationNodeStateTransition { case animateIn diff --git a/submodules/ContextUI/Sources/ContextSourceContainer.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextSourceContainer.swift similarity index 99% rename from submodules/ContextUI/Sources/ContextSourceContainer.swift rename to submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextSourceContainer.swift index 5692c6e8de..eb911f2e52 100644 --- a/submodules/ContextUI/Sources/ContextSourceContainer.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextSourceContainer.swift @@ -12,10 +12,11 @@ import PlainButtonComponent import MultilineTextComponent import ComponentDisplayAdapters import AccountContext +import ContextUI final class ContextSourceContainer: ASDisplayNode { final class Source { - weak var controller: ContextController? + weak var controller: ContextControllerImpl? let id: AnyHashable let title: String @@ -44,7 +45,7 @@ final class ContextSourceContainer: ASDisplayNode { private let actionsReady = Promise() init( - controller: ContextController, + controller: ContextControllerImpl, id: AnyHashable, title: String, footer: String?, @@ -359,7 +360,7 @@ final class ContextSourceContainer: ASDisplayNode { } } - private weak var controller: ContextController? + private weak var controller: ContextControllerImpl? private let backgroundNode: NavigationBackgroundNode @@ -387,7 +388,7 @@ final class ContextSourceContainer: ASDisplayNode { return self.activeSource?.presentationNode.wantsDisplayBelowKeyboard() ?? false } - init(controller: ContextController, configuration: ContextController.Configuration, context: AccountContext?) { + init(controller: ContextControllerImpl, configuration: ContextController.Configuration, context: AccountContext?) { self.controller = controller self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: false) diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/PeekController.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/PeekController.swift new file mode 100644 index 0000000000..6ce55e630a --- /dev/null +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/PeekController.swift @@ -0,0 +1,120 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramPresentationData +import ContextUI + +public final class PeekControllerImpl: ViewController, PeekController, ContextControllerProtocol { + public var useComplexItemsTransitionAnimation: Bool = false + public var immediateItemsTransitionAnimation = false + + public func getActionsMinHeight() -> ContextController.ActionsHeight? { + return nil + } + + public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, animated: Bool) { + } + + public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { + } + + public func pushItems(items: Signal) { + self.controllerNode.pushItems(items: items) + } + + public func popItems() { + self.controllerNode.popItems() + } + + private var controllerNode: PeekControllerNode { + return self.displayNode as! PeekControllerNode + } + + public var contentNode: PeekControllerContentNode & ASDisplayNode { + return self.controllerNode.contentNode + } + + private let presentationData: PresentationData + private let content: PeekControllerContent + public var sourceView: () -> (UIView, CGRect)? + private let activateImmediately: Bool + + public var visibilityUpdated: ((Bool) -> Void)? + + public var getOverlayViews: (() -> [UIView])? + + public var appeared: (() -> Void)? + public var disappeared: (() -> Void)? + + private var animatedIn = false + + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + public init(presentationData: PresentationData, content: PeekControllerContent, sourceView: @escaping () -> (UIView, CGRect)?, activateImmediately: Bool = false) { + self.presentationData = presentationData + self.content = content + self.sourceView = sourceView + self.activateImmediately = activateImmediately + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = PeekControllerNode(presentationData: self.presentationData, controller: self, content: self.content, requestDismiss: { [weak self] in + self?.dismiss() + }) + self.displayNodeDidLoad() + } + + private func getSourceRect() -> CGRect { + if let (sourceView, sourceRect) = self.sourceView() { + return sourceView.convert(sourceRect, to: self.view) + } else { + let size = self.displayNode.bounds.size + return CGRect(origin: CGPoint(x: floor((size.width - 10.0) / 2.0), y: floor((size.height - 10.0) / 2.0)), size: CGSize(width: 10.0, height: 10.0)) + } + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedIn { + self.animatedIn = true + self.controllerNode.animateIn(from: self.getSourceRect()) + + self.visibilityUpdated?(true) + + if self.activateImmediately { + self.controllerNode.activateMenu(immediately: true) + } + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, transition: transition) + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.visibilityUpdated?(false) + self.controllerNode.animateOut(to: self.getSourceRect(), completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + }) + } + + public func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) { + self.dismiss(completion: completion) + } +} diff --git a/submodules/ContextUI/Sources/PeekControllerNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/PeekControllerNode.swift similarity index 99% rename from submodules/ContextUI/Sources/PeekControllerNode.swift rename to submodules/TelegramUI/Components/ContextControllerImpl/Sources/PeekControllerNode.swift index 7400fe91cd..fb5e87015a 100644 --- a/submodules/ContextUI/Sources/PeekControllerNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/PeekControllerNode.swift @@ -4,10 +4,11 @@ import AsyncDisplayKit import Display import SwiftSignalKit import TelegramPresentationData +import ContextUI private let animationDurationFactor: Double = 1.0 -final class PeekControllerNode: ViewControllerTracingNode { +final class PeekControllerNode: ViewControllerTracingNode, PeekControllerNodeProtocol { private let requestDismiss: () -> Void private let presentationData: PresentationData @@ -79,7 +80,7 @@ final class PeekControllerNode: ViewControllerTracingNode { var activatedActionImpl: (() -> Void)? var requestLayoutImpl: ((ContainedViewLayoutTransition) -> Void)? - self.actionsStackNode = ContextControllerActionsStackNode( + self.actionsStackNode = ContextControllerActionsStackNodeImpl( context: nil, getController: { [weak controller] in return controller @@ -405,7 +406,7 @@ final class PeekControllerNode: ViewControllerTracingNode { } } - func activateMenu(immediately: Bool = false) { + func activateMenu(immediately: Bool) { if self.content.menuItems().isEmpty { if let fullScreenAccessoryNode = self.fullScreenAccessoryNode { fullScreenAccessoryNode.alpha = 1.0 diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/PinchController.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/PinchController.swift new file mode 100644 index 0000000000..886e11d0e5 --- /dev/null +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/PinchController.swift @@ -0,0 +1,262 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import TextSelectionNode +import TelegramCore +import SwiftSignalKit +import UIKitRuntimeUtils +import ContextUI + +private final class PinchControllerNode: ViewControllerTracingNode { + private weak var controller: PinchController? + + private var initialSourceFrame: CGRect? + + private let clippingNode: ASDisplayNode + private let scrollingContainer: ASDisplayNode + + private let sourceNode: PinchSourceContainerNode + private let disableScreenshots: Bool + private let getContentAreaInScreenSpace: () -> CGRect + + private let dimNode: ASDisplayNode + + private var validLayout: ContainerViewLayout? + private var isAnimatingOut: Bool = false + + private var hapticFeedback: HapticFeedback? + + init(controller: PinchController, sourceNode: PinchSourceContainerNode, disableScreenshots: Bool, getContentAreaInScreenSpace: @escaping () -> CGRect) { + self.controller = controller + self.sourceNode = sourceNode + self.disableScreenshots = disableScreenshots + self.getContentAreaInScreenSpace = getContentAreaInScreenSpace + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.dimNode.alpha = 0.0 + + self.clippingNode = ASDisplayNode() + self.clippingNode.clipsToBounds = true + + self.scrollingContainer = ASDisplayNode() + + super.init() + + self.addSubnode(self.dimNode) + self.addSubnode(self.clippingNode) + self.clippingNode.addSubnode(self.scrollingContainer) + + self.sourceNode.deactivate = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.controller?.dismiss() + } + + self.sourceNode.updated = { [weak self] scale, pinchLocation, offset in + guard let strongSelf = self, let initialSourceFrame = strongSelf.initialSourceFrame else { + return + } + strongSelf.dimNode.alpha = max(0.0, min(1.0, scale - 1.0)) + + let pinchOffset = CGPoint( + x: pinchLocation.x - initialSourceFrame.width / 2.0, + y: pinchLocation.y - initialSourceFrame.height / 2.0 + ) + + var transform = CATransform3DIdentity + transform = CATransform3DTranslate(transform, offset.x - pinchOffset.x * (scale - 1.0), offset.y - pinchOffset.y * (scale - 1.0), 0.0) + transform = CATransform3DScale(transform, scale, scale, 0.0) + + strongSelf.sourceNode.contentNode.transform = transform + } + + if self.disableScreenshots { + setLayerDisableScreenshots(self.layer, true) + } + } + + deinit { + } + + override func didLoad() { + super.didLoad() + } + + func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?) { + if self.isAnimatingOut { + return + } + + self.validLayout = layout + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + } + + func animateIn() { + let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view) + self.sourceNode.contentNode.frame = convertedFrame + self.initialSourceFrame = convertedFrame + self.scrollingContainer.addSubnode(self.sourceNode.contentNode) + + var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace() + updatedContentAreaInScreenSpace.origin.x = 0.0 + updatedContentAreaInScreenSpace.size.width = self.bounds.width + + self.clippingNode.layer.animateFrame(from: updatedContentAreaInScreenSpace, to: self.clippingNode.frame, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + self.clippingNode.layer.animateBoundsOriginYAdditive(from: updatedContentAreaInScreenSpace.minY, to: 0.0, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + + func animateOut(completion: @escaping () -> Void) { + self.isAnimatingOut = true + + let performCompletion: () -> Void = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.isAnimatingOut = false + + strongSelf.sourceNode.restoreToNaturalSize() + strongSelf.sourceNode.addSubnode(strongSelf.sourceNode.contentNode) + + strongSelf.sourceNode.animatedOut?() + + completion() + } + + let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view) + self.sourceNode.contentNode.frame = convertedFrame + self.initialSourceFrame = convertedFrame + + if let (scale, pinchLocation, offset) = self.sourceNode.gesture.currentTransform, let initialSourceFrame = self.initialSourceFrame { + let duration = 0.3 + let transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut + + var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace() + updatedContentAreaInScreenSpace.origin.x = 0.0 + updatedContentAreaInScreenSpace.size.width = self.bounds.width + + self.clippingNode.layer.animateFrame(from: self.clippingNode.frame, to: updatedContentAreaInScreenSpace, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) + self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) + + let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: .spring) + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.prepareImpact(.light) + self.hapticFeedback?.impact(.light) + + self.sourceNode.scaleUpdated?(1.0, transition) + + let pinchOffset = CGPoint( + x: pinchLocation.x - initialSourceFrame.width / 2.0, + y: pinchLocation.y - initialSourceFrame.height / 2.0 + ) + + var transform = CATransform3DIdentity + transform = CATransform3DScale(transform, scale, scale, 0.0) + + self.sourceNode.contentNode.transform = CATransform3DIdentity + self.sourceNode.contentNode.position = CGPoint(x: initialSourceFrame.midX, y: initialSourceFrame.midY) + self.sourceNode.contentNode.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration * 1.2, damping: 110.0) + self.sourceNode.contentNode.layer.animatePosition(from: CGPoint(x: offset.x - pinchOffset.x * (scale - 1.0), y: offset.y - pinchOffset.y * (scale - 1.0)), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { _ in + performCompletion() + }) + + let dimNodeTransition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: transitionCurve) + dimNodeTransition.updateAlpha(node: self.dimNode, alpha: 0.0) + } else { + performCompletion() + } + } + + func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + if self.isAnimatingOut { + self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset.y) + transition.animateOffsetAdditive(node: self.scrollingContainer, offset: -offset.y) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return nil + } +} + +public final class PinchControllerImpl: ViewController, PinchController, StandalonePresentableController { + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + private let sourceNode: PinchSourceContainerNode + private let disableScreenshots: Bool + private let getContentAreaInScreenSpace: () -> CGRect + + private var wasDismissed = false + + private var controllerNode: PinchControllerNode { + return self.displayNode as! PinchControllerNode + } + + public init(sourceNode: PinchSourceContainerNode, disableScreenshots: Bool = false, getContentAreaInScreenSpace: @escaping () -> CGRect) { + self.sourceNode = sourceNode + self.disableScreenshots = disableScreenshots + self.getContentAreaInScreenSpace = getContentAreaInScreenSpace + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + + self.lockOrientation = true + self.blocksBackgroundWhenInOverlay = true + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func loadDisplayNode() { + self.displayNode = PinchControllerNode(controller: self, sourceNode: self.sourceNode, disableScreenshots: self.disableScreenshots, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) + + self.displayNodeDidLoad() + + self._ready.set(.single(true)) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.updateLayout(layout: layout, transition: transition, previousActionsContainerNode: nil) + } + + override public func viewDidAppear(_ animated: Bool) { + if self.ignoreAppearanceMethodInvocations() { + return + } + super.viewDidAppear(animated) + + self.controllerNode.animateIn() + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.wasDismissed { + self.wasDismissed = true + self.controllerNode.animateOut(completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + completion?() + }) + } + } + + public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + self.controllerNode.addRelativeContentOffset(offset, transition: transition) + } +} diff --git a/submodules/ContextUI/Sources/ReactionPreviewView.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ReactionPreviewView.swift similarity index 100% rename from submodules/ContextUI/Sources/ReactionPreviewView.swift rename to submodules/TelegramUI/Components/ContextControllerImpl/Sources/ReactionPreviewView.swift diff --git a/submodules/TelegramUI/Components/EdgeEffect/Sources/EdgeEffect.swift b/submodules/TelegramUI/Components/EdgeEffect/Sources/EdgeEffect.swift index cad11e4241..b1ef32302c 100644 --- a/submodules/TelegramUI/Components/EdgeEffect/Sources/EdgeEffect.swift +++ b/submodules/TelegramUI/Components/EdgeEffect/Sources/EdgeEffect.swift @@ -598,6 +598,84 @@ public final class VariableBlurView: UIView { variableBlur.setValue("mask_source", forKey: "inputSourceSublayerName") variableBlur.setValue(true, forKey: "inputNormalizeEdges") mainEffectLayer.filters = [variableBlur] + + /*#if DEBUG + if let classValue = NSClassFromString("CAFilter") as AnyObject as? NSObjectProtocol { + let makeSelector = NSSelectorFromString("filterWithName:") + let filter = classValue.perform(makeSelector, with: "colorMatrix").takeUnretainedValue() as? NSObject + + if let filter { + /// Builds a 4x5 (RGBA) color matrix (20 floats, row-major). + /// Applies: Desaturate(s) then TintToward(t, k) in linear RGB. + /// - s: saturation in [0,1] (1 = unchanged, 0 = grayscale) + /// - k: tint strength in [0,1] (0 = none, 1 = fully "brightness->tint" mapping) + /// - t: tint color (linear RGB). Doesn't have to be normalized, but should be non-negative. + /// - bias: optional constant "cast" added in the bias column, scaled by tint color. + let makeDesatTintMatrix: (Float32, Float32, (Float32, Float32, Float32), Float32) -> [Float32] = { s, k, t, bias in + // Rec.709 luma weights (good default for linear RGB) + let wr: Float32 = 0.2126 + let wg: Float32 = 0.7152 + let wb: Float32 = 0.0722 + + let (tr, tg, tb) = t + + // --- D = desaturation matrix --- + let a = 1 - s + + let D00 = wr * a + s, D01 = wg * a, D02 = wb * a + let D10 = wr * a, D11 = wg * a + s, D12 = wb * a + let D20 = wr * a, D21 = wg * a, D22 = wb * a + s + + // --- T = outer(t, w): maps luma to tinted RGB (preserves luma per weights) --- + let T00 = tr * wr, T01 = tr * wg, T02 = tr * wb + let T10 = tg * wr, T11 = tg * wg, T12 = tg * wb + let T20 = tb * wr, T21 = tb * wg, T22 = tb * wb + + // --- A = (1-k)I + kT --- + let ik = 1 - k + + let A00 = ik + k * T00, A01 = k * T01, A02 = k * T02 + let A10 = k * T10, A11 = ik + k * T11, A12 = k * T12 + let A20 = k * T20, A21 = k * T21, A22 = ik + k * T22 + + // --- M = A * D (3x3 multiply) --- + let M00 = A00 * D00 + A01 * D10 + A02 * D20 + let M01 = A00 * D01 + A01 * D11 + A02 * D21 + let M02 = A00 * D02 + A01 * D12 + A02 * D22 + + let M10 = A10 * D00 + A11 * D10 + A12 * D20 + let M11 = A10 * D01 + A11 * D11 + A12 * D21 + let M12 = A10 * D02 + A11 * D12 + A12 * D22 + + let M20 = A20 * D00 + A21 * D10 + A22 * D20 + let M21 = A20 * D01 + A21 * D11 + A22 * D21 + let M22 = A20 * D02 + A21 * D12 + A22 * D22 + + // Optional constant cast in bias column + let bR = bias * tr + let bG = bias * tg + let bB = bias * tb + + // 4x5 row-major, alpha passthrough: + return [ + M00, M01, M02, 0, bR, + M10, M11, M12, 0, bG, + M20, M21, M22, 0, bB, + 0, 0, 0, 1, 0 + ] + } + + var matrix: [Float32] = makeDesatTintMatrix( + 0.1, // more desaturated + 0.1, // moderate tinting + (0.0, 1.0, 0.0), // warm tint target + 0.0 // set e.g. 0.03 for a gentle warm cast + ) + filter.setValue(NSValue(bytes: &matrix, objCType: "{CAColorMatrix=ffffffffffffffffffff}"), forKey: "inputColorMatrix") + mainEffectLayer.filters = [variableBlur, filter] + } + } + #endif*/ } } else { self.additionalEffectLayer = createBackdropLayer() diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD index 16f3589b91..4b2ada38af 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD @@ -48,6 +48,7 @@ swift_library( "//submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView", "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/ContextControllerImpl", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift index da8216443b..726eeca4e8 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift @@ -8,6 +8,7 @@ import TelegramCore import AccountContext import TelegramPresentationData import ContextUI +import ContextControllerImpl final class GiftAttributeListContextItem: ContextMenuCustomItem { let context: AccountContext diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index 313b5508d5..8417e332bb 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -511,7 +511,7 @@ final class GiftStoreScreenComponent: Component { self.scrollToTop() }))) - let contextController = ContextController(presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + let contextController = makeContextController(presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) contextController.dismissed = { [weak self] in guard let self else { return @@ -613,7 +613,7 @@ final class GiftStoreScreenComponent: Component { } ), false)) - let contextController = ContextController( + let contextController = makeContextController( context: component.context, presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), @@ -721,7 +721,7 @@ final class GiftStoreScreenComponent: Component { } ), false)) - let contextController = ContextController( + let contextController = makeContextController( context: component.context, presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), @@ -829,7 +829,7 @@ final class GiftStoreScreenComponent: Component { } ), false)) - let contextController = ContextController( + let contextController = makeContextController( context: component.context, presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift index d65fbb44f8..3155acb8ad 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift @@ -1831,7 +1831,7 @@ private final class GiftAuctionBidScreenComponent: Component { self?.share() }))) - let contextController = ContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: controller, sourceView: view)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: controller, sourceView: view)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) controller.presentInGlobalOverlay(contextController) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index 9f6a5f722b..abff1881ba 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -459,7 +459,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { self?.share() }))) - let contextController = ContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: controller, sourceView: view)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: controller, sourceView: view)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) controller.presentInGlobalOverlay(contextController) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 9a1d54fd96..10f03880e4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -1530,7 +1530,7 @@ private final class GiftViewSheetContent: CombinedComponent { }))) } - let contextController = ContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) controller.presentInGlobalOverlay(contextController) }) } diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift index ce27144179..5744d55761 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift @@ -424,7 +424,9 @@ public class GlassBackgroundView: UIView { nativeView.frame = CGRect(origin: CGPoint(), size: size) } else { let nativeFrame = CGRect(origin: CGPoint(), size: size) - transition.setFrame(view: nativeView, frame: nativeFrame) + transition.animateView { + nativeView.frame = nativeFrame + } } nativeView.overrideUserInterfaceStyle = isDark ? .dark : .light } @@ -544,7 +546,7 @@ public class GlassBackgroundView: UIView { if transition.animation.isImmediate { nativeView.effect = nil } else { - UIView.animate { + transition.animateView { nativeView.effect = nil } } @@ -555,7 +557,7 @@ public class GlassBackgroundView: UIView { } else { if let glassEffect, let currentEffect = nativeView.effect as? UIGlassEffect, currentEffect.tintColor == glassEffect.tintColor, currentEffect.isInteractive == glassEffect.isInteractive { } else { - UIView.animate { + transition.animateView { nativeView.effect = glassEffect } } @@ -680,7 +682,9 @@ public final class GlassBackgroundContainerView: UIView { nativeParamsView.lumaMax = 0.801 } - transition.setFrame(view: nativeView, frame: CGRect(origin: CGPoint(), size: size)) + transition.animateView { + nativeView.frame = CGRect(origin: CGPoint(), size: size) + } } else if let legacyView = self.legacyView { transition.setFrame(view: legacyView, frame: CGRect(origin: CGPoint(), size: size)) } @@ -1082,3 +1086,136 @@ public final class GlassBackgroundComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +public final class GlassContextExtractableContainer: UIView, ContextExtractableContainer { + private struct NormalParams { + let size: CGSize + let cornerRadius: CGFloat + let isDark: Bool + let tintColor: GlassBackgroundView.TintColor + let isInteractive: Bool + let isVisible: Bool + + init(size: CGSize, cornerRadius: CGFloat, isDark: Bool, tintColor: GlassBackgroundView.TintColor, isInteractive: Bool, isVisible: Bool) { + self.size = size + self.cornerRadius = cornerRadius + self.isDark = isDark + self.tintColor = tintColor + self.isInteractive = isInteractive + self.isVisible = isVisible + } + } + + public let extractableContentView: UIView + public let normalContentView: UIView + + public var contentView: UIView { + return self.normalContentView + } + + private let glassView: GlassBackgroundView + + private var state: State = .normal + private var normalParams: NormalParams? + + override public init(frame: CGRect) { + self.extractableContentView = UIView() + self.glassView = GlassBackgroundView() + self.normalContentView = SparseContainerView() + + super.init(frame: frame) + + self.glassView.contentView.addSubview(self.normalContentView) + self.extractableContentView.addSubview(self.glassView) + self.addSubview(self.extractableContentView) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.isUserInteractionEnabled { + return nil + } + if self.isHidden { + return nil + } + if self.alpha == 0.0 { + return nil + } + switch self.state { + case .normal: + if let result = self.normalContentView.hitTest(self.convert(point, to: self.normalContentView), with: event) { + return result + } + case .extracted: + break + } + + return nil + } + + public func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, tintColor: GlassBackgroundView.TintColor, isInteractive: Bool = false, isVisible: Bool = true, transition: ComponentTransition) { + let normalParams = NormalParams(size: size, cornerRadius: cornerRadius, isDark: isDark, tintColor: tintColor, isInteractive: isInteractive, isVisible: isVisible) + self.normalParams = normalParams + + if case .normal = self.state { + self.applyState(transition: transition) + } + } + + public func updateState(state: State, transition: ContainedViewLayoutTransition) { + self.state = state + self.applyState(transition: ComponentTransition(transition)) + } + + private func applyState(transition: ComponentTransition) { + guard let normalParams = self.normalParams else { + return + } + switch self.state { + case .normal: + transition.setAlpha(view: self.normalContentView, alpha: 1.0) + transition.setFrame(view: self.extractableContentView, frame: CGRect(origin: CGPoint(), size: normalParams.size)) + transition.setFrame(view: self.normalContentView, frame: CGRect(origin: CGPoint(), size: normalParams.size)) + + self.glassView.update( + size: normalParams.size, + cornerRadius: normalParams.cornerRadius, + isDark: normalParams.isDark, + tintColor: normalParams.tintColor, + isInteractive: normalParams.isInteractive, + isVisible: normalParams.isVisible, + transition: transition + ) + case let .extracted(size, cornerRadius, extractionState): + switch extractionState { + case .animatedOut: + transition.setAlpha(view: self.normalContentView, alpha: 1.0) + + self.glassView.update( + size: normalParams.size, + cornerRadius: normalParams.cornerRadius, + isDark: normalParams.isDark, + tintColor: normalParams.tintColor, + isInteractive: normalParams.isInteractive, + isVisible: normalParams.isVisible, + transition: transition + ) + case .animatedIn: + transition.setAlpha(view: self.normalContentView, alpha: 0.0) + + self.glassView.update( + size: size, + cornerRadius: cornerRadius, + isDark: normalParams.isDark, + tintColor: normalParams.tintColor, + isInteractive: normalParams.isInteractive, + isVisible: normalParams.isVisible, + transition: transition + ) + } + } + } +} diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index 48808f8172..99893fe804 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -435,7 +435,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { updateTimeout(nil) }))) - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(sourceView: sourceView, position: self.currentIsCaptionAbove ? .bottom : .top)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(sourceView: sourceView, position: self.currentIsCaptionAbove ? .bottom : .top)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.present(contextController) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index 448ecd6c1b..229c8257ae 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -70,6 +70,7 @@ swift_library( "//submodules/TelegramNotices", "//submodules/TelegramUI/Components/AttachmentFileController", "//submodules/SaveToCameraRoll", + "//submodules/TelegramUI/Components/ContextControllerImpl", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift index 8dbfb6af85..6e33cd4f93 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift @@ -25,7 +25,7 @@ func presentLinkOptionsController(context: AccountContext, selfController: Creat return } - let contextController = ContextController( + let contextController = makeContextController( presentationData: context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme), configuration: ContextController.Configuration( sources: sources, diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 481ad1ee59..682ca3fc7d 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -5406,7 +5406,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) - let contextController = ContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint(x: 0.0, y: -3.0))), items: .single(ContextController.Items(content: .list(items)))) + let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint(x: 0.0, y: -3.0))), items: .single(ContextController.Items(content: .list(items)))) self.controller?.present(contextController, in: .window(.root)) } @@ -7224,7 +7224,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self?.node.presentAudioPicker() }))) - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.present(contextController, in: .window(.root)) } @@ -7300,7 +7300,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID ))) } - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.present(contextController, in: .window(.root)) } @@ -7916,7 +7916,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID portalView.view.layer.rasterizationScale = UIScreenScale self.node.previewContentContainerView.addPortal(view: portalView) - let stickerResultController = PeekController( + let stickerResultController = makePeekController( presentationData: presentationData, content: StickerPreviewPeekContent( context: self.context, diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift index e33448bbe4..a5642fb04e 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift @@ -9,6 +9,7 @@ import AccountContext import TelegramPresentationData import StickerResources import ContextUI +import ContextControllerImpl final class StickerPackListContextItem: ContextMenuCustomItem { let context: AccountContext diff --git a/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift index 387df50b9c..01562c6935 100644 --- a/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -586,7 +586,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi let items = self.contextMenuSpeedItems(scheduleTooltip: { change in scheduledTooltip = change }) - let contextController = ContextController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.rateButton.referenceNode, shouldBeDismissed: self.dismissedPromise.get())), items: items, gesture: gesture) + let contextController = makeContextController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.rateButton.referenceNode, shouldBeDismissed: self.dismissedPromise.get())), items: items, gesture: gesture) contextController.dismissed = { [weak self] in if let scheduledTooltip, let self, let rate = self.playbackBaseRate { self.setRate?(rate, scheduledTooltip) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/StickersResultPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/StickersResultPanelComponent.swift index 0e1c47857d..27bdcf2116 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/StickersResultPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/StickersResultPanelComponent.swift @@ -290,7 +290,7 @@ final class StickersResultPanelComponent: Component { }, present: { [weak self] content, sourceView, sourceRect in if let self, let component = self.component { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: component.theme) - let controller = PeekController(presentationData: presentationData, content: content, sourceView: { + let controller = makePeekController(presentationData: presentationData, content: content, sourceView: { return (sourceView, sourceRect) }) component.presentInGlobalOverlay(controller) diff --git a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift index a0bee77972..1c743b30e9 100644 --- a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift +++ b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift @@ -550,8 +550,8 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { public let backgroundNode: NavigationBackgroundNode - private var leftButtonsBackgroundView: (background: GlassBackgroundView, container: UIView)? - private var rightButtonsBackgroundView: (background: GlassBackgroundView, container: UIView)? + private var leftButtonsBackgroundView: (background: GlassContextExtractableContainer, container: UIView)? + private var rightButtonsBackgroundView: (background: GlassContextExtractableContainer, container: UIView)? private let backButtonNodeImpl: NavigationButtonNodeImpl public var backButtonNode: NavigationButtonNode { @@ -662,12 +662,12 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { backgroundContainer.contentView.addSubview(self.customOverBackgroundContentView) - let leftButtonsBackgroundView: (background: GlassBackgroundView, container: UIView) = (GlassBackgroundView(), UIView()) + let leftButtonsBackgroundView: (background: GlassContextExtractableContainer, container: UIView) = (GlassContextExtractableContainer(), UIView()) leftButtonsBackgroundView.background.contentView.addSubview(leftButtonsBackgroundView.container) self.leftButtonsBackgroundView = leftButtonsBackgroundView backgroundContainer.contentView.addSubview(leftButtonsBackgroundView.background) - let rightButtonsBackgroundView: (background: GlassBackgroundView, container: UIView) = (GlassBackgroundView(), UIView()) + let rightButtonsBackgroundView: (background: GlassContextExtractableContainer, container: UIView) = (GlassContextExtractableContainer(), UIView()) rightButtonsBackgroundView.background.contentView.addSubview(rightButtonsBackgroundView.container) self.rightButtonsBackgroundView = rightButtonsBackgroundView backgroundContainer.contentView.addSubview(rightButtonsBackgroundView.background) @@ -1062,6 +1062,16 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { } } + public func navigationButtonContextContainer(sourceView: UIView) -> ContextExtractableContainer? { + if let leftButtonsBackgroundView = self.leftButtonsBackgroundView, sourceView.isDescendant(of: leftButtonsBackgroundView.background) { + return leftButtonsBackgroundView.background + } + if let rightButtonsBackgroundView = self.rightButtonsBackgroundView, sourceView.isDescendant(of: rightButtonsBackgroundView.background) { + return rightButtonsBackgroundView.background + } + return nil + } + public var intrinsicCanTransitionInline: Bool = true public var passthroughTouches = true diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift index 73d835958e..276942da8a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift @@ -505,7 +505,7 @@ final class AffiliateProgramSetupScreenComponent: Component { } let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: false)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: false)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil) controller.presentInGlobalOverlay(contextController) } @@ -1343,7 +1343,7 @@ final class AffiliateProgramSetupScreenComponent: Component { let items = ContextController.Items(content: .list(itemList)) - let controller = ContextController( + let controller = makeContextController( presentationData: presentationData, source: .extracted(ListContextExtractedContentSource(contentView: sourceView)), items: .single(items), diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift index 3df157c9fd..d0620b4ee1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift @@ -450,7 +450,7 @@ private final class JoinAffiliateProgramScreenComponent: Component { }))) } - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: true)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: true)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil) controller.presentInGlobalOverlay(contextController) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift index ba66c7c4c6..16a1c5d10e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift @@ -389,7 +389,7 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, AS chatController.canReadHistory.set(false) let source: ContextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: parentController.navigationController as? NavigationController)) - let contextController = ContextController(presentationData: self.presentationData, source: source, items: savedMessagesPeerMenuItems(context: self.context, threadId: threadId, parentController: parentController, deletePeerChat: { [weak self] peerId in + let contextController = makeContextController(presentationData: self.presentationData, source: source, items: savedMessagesPeerMenuItems(context: self.context, threadId: threadId, parentController: parentController, deletePeerChat: { [weak self] peerId in guard let self else { return } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift index 6582a26c5b..92eea31088 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift @@ -443,7 +443,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { let dismissPromise = ValuePromise(false) let source = PeerInfoMemberExtractedContentSource(sourceNode: node, keepInPlace: false, blurBackground: true, centerVertically: false, shouldBeDismissed: dismissPromise.get()) - let contextController = ContextController(presentationData: presentationData, source: .extracted(source), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .extracted(source), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.parentController?.presentInGlobalOverlay(contextController) }) self.enclosingPeer = enclosingPeer diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarListNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarListNode.swift index 56e7f7f910..890c446de7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarListNode.swift @@ -103,7 +103,7 @@ final class PeerInfoAvatarListNode: ASDisplayNode { guard let strongSelf = self, let (_, _, _, _, _, isExpanded) = strongSelf.arguments, isExpanded else { return } - let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + let pinchController = makePinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { return UIScreen.main.bounds }) context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift index 07b6d1690b..108b8632f1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift @@ -36,8 +36,8 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { private var presentationData: PresentationData? private let backgroundContainer: GlassBackgroundContainerView - private let leftButtonsBackground: GlassBackgroundView - private let rightButtonsBackground: GlassBackgroundView + let leftButtonsBackground: GlassContextExtractableContainer + let rightButtonsBackground: GlassContextExtractableContainer private let leftButtonsContainer: UIView private let rightButtonsContainer: UIView @@ -55,8 +55,8 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { override init() { self.backgroundContainer = GlassBackgroundContainerView() - self.leftButtonsBackground = GlassBackgroundView() - self.rightButtonsBackground = GlassBackgroundView() + self.leftButtonsBackground = GlassContextExtractableContainer() + self.rightButtonsBackground = GlassContextExtractableContainer() self.leftButtonsContainer = UIView() self.leftButtonsContainer.clipsToBounds = true diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 152ed24816..6d718adb8e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -899,7 +899,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat } }))) - let contextController = ContextController( + let contextController = makeContextController( presentationData: params.presentationData, source: .reference(TabsReferenceContentSource(sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 931b9ee12f..70819bdb50 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -850,7 +850,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }))) } - let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + let controller = makeContextController(presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) strongSelf.controller?.window?.presentInGlobalOverlay(controller) }) }, openMessageReactionContextMenu: { _, _, _, _ in @@ -1006,7 +1006,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro switch previewData { case let .gallery(gallery): gallery.setHintWillBePresentedInPreviewingContext(true) - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node, sourceRect: rect)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node, sourceRect: rect)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.controller?.presentInGlobalOverlay(contextController) case .instantPage: break @@ -1293,7 +1293,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro })) ] } - let contextController = ContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) controller.presentInGlobalOverlay(contextController) } @@ -2325,7 +2325,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, synchronousLoad: true) galleryController.setHintWillBePresentedInPreviewingContext(true) - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.controller?.presentInGlobalOverlay(contextController) } @@ -5075,7 +5075,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, 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) + let contextController = makeContextController(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) } ) @@ -6827,7 +6827,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc })) }))) } - let contextController = ContextController(presentationData: presentationData, source: .reference(PeerInfoControllerContextReferenceContentSource(controller: parentController, sourceView: backButtonView, insets: UIEdgeInsets(), contentInsets: UIEdgeInsets(top: 0.0, left: -15.0, bottom: 0.0, right: -15.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(PeerInfoControllerContextReferenceContentSource(controller: parentController, sourceView: backButtonView, insets: UIEdgeInsets(), contentInsets: UIEdgeInsets(top: 0.0, left: -15.0, bottom: 0.0, right: -15.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) parentController.presentInGlobalOverlay(contextController) }) } @@ -6928,7 +6928,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc })))*/ } - let controller = ContextController(presentationData: self.presentationData, source: .reference(SettingsTabBarContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + let controller = makeContextController(presentationData: self.presentationData, source: .reference(SettingsTabBarContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) } @@ -7043,7 +7043,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc }))) let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: sourceController, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: sourceController, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) sourceController.presentInGlobalOverlay(contextController) }) } @@ -7177,15 +7177,20 @@ final class PeerInfoContextExtractedContentSource: ContextExtractedContentSource final class PeerInfoContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController - private let sourceNode: ContextReferenceContentNode + private let sourceView: UIView - init(controller: ViewController, sourceNode: ContextReferenceContentNode) { + init(controller: ViewController, sourceNode: ASDisplayNode) { self.controller = controller - self.sourceNode = sourceNode + self.sourceView = sourceNode.view + } + + init(controller: ViewController, sourceView: UIView) { + self.controller = controller + self.sourceView = sourceView } func transitionInfo() -> ContextControllerReferenceViewInfo? { - return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenBusinessActions.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenBusinessActions.swift index fe79739222..a2d2af6767 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenBusinessActions.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenBusinessActions.swift @@ -86,7 +86,7 @@ extension PeerInfoScreenNode { let actions = ContextController.Items(content: .list(items)) - let contextController = ContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) self.controller?.present(contextController, in: .window(.root)) } @@ -176,7 +176,7 @@ extension PeerInfoScreenNode { let actions = ContextController.Items(content: .list(items)) - let contextController = ContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) self.controller?.present(contextController, in: .window(.root)) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift index c3b58b3308..f27cded7ca 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift @@ -223,7 +223,7 @@ extension PeerInfoScreenNode { } if let sourceNode = strongSelf.headerNode.buttonNodes[.voiceChat]?.referenceNode, let controller = strongSelf.controller { - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.state = strongSelf.state.withHighlightedButton(nil) @@ -344,7 +344,7 @@ extension PeerInfoScreenNode { } if let sourceNode = strongSelf.headerNode.buttonNodes[.voiceChat]?.referenceNode, let controller = strongSelf.controller { - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.state = strongSelf.state.withHighlightedButton(nil) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayGiftsContextMenu.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayGiftsContextMenu.swift index 6d1d859c60..d767bdda1e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayGiftsContextMenu.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayGiftsContextMenu.swift @@ -222,7 +222,14 @@ extension PeerInfoScreenNode { return ContextController.Items(content: .list(items)) } - let contextController = ContextController(presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: items, gesture: gesture) + var sourceView: UIView = source.view + if sourceView.isDescendant(of: self.headerNode.navigationButtonContainer.rightButtonsBackground) { + sourceView = self.headerNode.navigationButtonContainer.rightButtonsBackground + } else if sourceView.isDescendant(of: self.headerNode.navigationButtonContainer.leftButtonsBackground) { + sourceView = self.headerNode.navigationButtonContainer.leftButtonsBackground + } + + let contextController = makeContextController(presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: items, gesture: gesture) contextController.passthroughTouchEvent = { [weak self] sourceView, point in guard let strongSelf = self else { return .ignore diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayMediaGalleryContextMenu.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayMediaGalleryContextMenu.swift index 280fd113c5..97da4e36fb 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayMediaGalleryContextMenu.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayMediaGalleryContextMenu.swift @@ -102,7 +102,7 @@ extension PeerInfoScreenNode { }))) } - let contextController = ContextController(presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(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 @@ -232,7 +232,7 @@ extension PeerInfoScreenNode { }))) } - let contextController = ContextController(presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(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 @@ -408,7 +408,14 @@ extension PeerInfoScreenNode { }))) } - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + var sourceView: UIView = source.view + if sourceView.isDescendant(of: strongSelf.headerNode.navigationButtonContainer.rightButtonsBackground) { + sourceView = strongSelf.headerNode.navigationButtonContainer.rightButtonsBackground + } else if sourceView.isDescendant(of: strongSelf.headerNode.navigationButtonContainer.leftButtonsBackground) { + sourceView = strongSelf.headerNode.navigationButtonContainer.leftButtonsBackground + } + + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) contextController.passthroughTouchEvent = { sourceView, point in guard let strongSelf = self else { return .ignore diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBio.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBio.swift index a273ef433e..3b789a30a7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBio.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBio.swift @@ -122,7 +122,7 @@ extension PeerInfoScreenNode { let actions = ContextController.Items(content: .list(items)) - let contextController = ContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) self.controller?.present(contextController, in: .window(.root)) }) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBirthday.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBirthday.swift index 95c79937eb..5aab30eef2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBirthday.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBirthday.swift @@ -63,7 +63,7 @@ extension PeerInfoScreenNode { let actions = ContextController.Items(content: .list(items)) - let contextController = ContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) self.controller?.present(contextController, in: .window(.root)) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenNote.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenNote.swift index 9d2ff54987..2db0ca69fe 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenNote.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenNote.swift @@ -70,7 +70,7 @@ extension PeerInfoScreenNode { let actions = ContextController.Items(content: .list(items)) - let contextController = ContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) self.controller?.present(contextController, in: .window(.root)) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenPhone.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenPhone.swift index 8219c2df55..9cfa3a0f54 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenPhone.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenPhone.swift @@ -172,7 +172,7 @@ extension PeerInfoScreenNode { }) }) } - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) strongSelf.controller?.present(contextController, in: .window(.root)) }) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenUsername.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenUsername.swift index b9ee8fea14..94896590ed 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenUsername.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenUsername.swift @@ -51,7 +51,7 @@ extension PeerInfoScreenNode { let actions = ContextController.Items(content: .list(items)) - let contextController = ContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) self.controller?.present(contextController, in: .window(.root)) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift index 8ac338ed51..05f0178ac0 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift @@ -375,7 +375,7 @@ extension PeerInfoScreenNode { self.view.endEditing(true) if let sourceNode = self.headerNode.buttonNodes[.mute]?.referenceNode { - let contextController = ContextController(presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items), tip: tip)), gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items), tip: tip)), gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.state = strongSelf.state.withHighlightedButton(nil) @@ -1187,7 +1187,7 @@ extension PeerInfoScreenNode { if let sourceNode = self.headerNode.buttonNodes[.more]?.referenceNode { let items = mainItemsImpl?() ?? .single([]) - let contextController = ContextController(presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.state = strongSelf.state.withHighlightedButton(nil) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsTabActions.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsTabActions.swift index 5c9bb8c78c..fa16166d34 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsTabActions.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsTabActions.swift @@ -53,7 +53,7 @@ extension PeerInfoScreenNode { let accountContext = self.context.sharedContext.makeTempAccountContext(account: selectedAccount) let chatListController = accountContext.sharedContext.makeChatListController(context: accountContext, location: .chatList(groupId: EngineChatList.Group(.root)), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) - let contextController = ContextController(presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node)), items: accountContextMenuItems(context: accountContext, logout: { [weak self] in + let contextController = makeContextController(presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node)), items: accountContextMenuItems(context: accountContext, logout: { [weak self] in self?.logoutAccount(id: id) }) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) self.controller?.presentInGlobalOverlay(contextController) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index 925c3098b3..7534cbb812 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -201,7 +201,7 @@ final class PeerInfoStoryGridScreenComponent: Component { } } - let contextController = ContextController(presentationData: presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + let contextController = makeContextController(presentationData: presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) contextController.passthroughTouchEvent = { [weak self] sourceView, point in guard let self else { return .ignore diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift index 6088448007..5c5b6d91e1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift @@ -429,7 +429,7 @@ public final class AddGiftsScreen: ViewControllerComponentContainer { return ContextController.Items(content: .list(items)) } - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: items, gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: items, gesture: gesture) self.presentInGlobalOverlay(contextController) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index d9a85eb9f0..5d74dac80c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -566,7 +566,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr }))) } - let contextController = ContextController( + let contextController = makeContextController( presentationData: params.presentationData, source: .extracted(GiftsExtractedContentSource(sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), @@ -1423,7 +1423,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } let previewController = GiftContextPreviewController(context: self.context, gift: gift) - let contextController = ContextController( + let contextController = makeContextController( context: self.context, presentationData: currentParams.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: previewController, sourceView: view)), diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 4df8073aa4..b0292ae644 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -2868,7 +2868,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.tempContextContentItemNode = tempSourceNode self.addSubnode(tempSourceNode) - let contextController = ContextController(presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: parentController, sourceNode: tempSourceNode.contextSourceNode, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: parentController, sourceNode: tempSourceNode.contextSourceNode, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) parentController.presentInGlobalOverlay(contextController) } @@ -3988,7 +3988,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } let presentationData = self.presentationData - let contextController = ContextController( + let contextController = makeContextController( presentationData: presentationData, source: .extracted(ItemExtractedContentSource( sourceNode: sourceNode, diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index dbf4b785ec..9a31a4eeea 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -578,7 +578,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { return items } - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceView: sourceNode, passthroughTouches: true)), items: items |> map { ContextController.Items(content: .list($0)) }) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceView: sourceNode, passthroughTouches: true)), items: items |> map { ContextController.Items(content: .list($0)) }) contextController.dismissedForCancel = { [weak chatController] in if let selectedMessageIds = chatController?.selectedMessageIds { var forwardMessageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift index d7d1e46641..6d359ac7bc 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift @@ -577,7 +577,7 @@ final class BusinessLinksSetupScreenComponent: Component { )) let items = ContextController.Items(content: .list(itemList)) - let controller = ContextController( + let controller = makeContextController( presentationData: presentationData, source: .extracted(BusineesLinkListContextExtractedContentSource(contentView: sourceView)), items: .single(items), recognizer: nil, gesture: gesture) diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 44e173dfaa..822bac6fd8 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -979,7 +979,7 @@ final class ShareWithPeersScreenComponent: Component { }))) } - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: true)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: true)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil) controller.presentInGlobalOverlay(contextController) } } diff --git a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift index c5631fee10..1b78ec9f76 100644 --- a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift +++ b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift @@ -825,7 +825,7 @@ public class StickerPickerScreen: ViewController { }))) } - let contextController = ContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) controller.presentInGlobalOverlay(contextController) }) } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 16098f337b..b3a0b625bd 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -2088,7 +2088,7 @@ final class StorageUsageScreenComponent: Component { )) let items = ContextController.Items(content: .list(itemList)) - let controller = ContextController( + let controller = makeContextController( presentationData: presentationData, source: .extracted(StorageUsageListContextExtractedContentSource(contentView: sourceView)), items: .single(items), recognizer: nil, gesture: gesture) @@ -2620,7 +2620,7 @@ final class StorageUsageScreenComponent: Component { switch previewData { case let .gallery(gallery): gallery.setHintWillBePresentedInPreviewingContext(true) - let contextController = ContextController( + let contextController = makeContextController( presentationData: presentationData, source: .controller(StorageUsageListContextGalleryContentSourceImpl( controller: gallery, @@ -2723,7 +2723,7 @@ final class StorageUsageScreenComponent: Component { )) let items = ContextController.Items(content: .list(itemList)) - let controller = ContextController( + let controller = makeContextController( presentationData: presentationData, source: .extracted(StorageUsageListContextExtractedContentSource(contentView: sourceView)), items: .single(items), recognizer: nil, gesture: gesture) @@ -3280,7 +3280,7 @@ final class StorageUsageScreenComponent: Component { let items: Signal = .single(ContextController.Items(content: .list(subItems))) let source: ContextContentSource = .reference(StorageUsageContextReferenceContentSource(sourceView: sourceLabelView)) - let contextController = ContextController( + let contextController = makeContextController( presentationData: presentationData, source: source, items: items, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift index f74712273d..dd9701b798 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift @@ -517,7 +517,7 @@ final class StoryContentLiveChatComponent: Component { }))) } - let contextController = ContextController( + let contextController = makeContextController( presentationData: presentationData, source: .extracted(ItemExtractedContentSource( sourceNode: sourceNode, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index a037a504ca..9f984e1bc3 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -3826,7 +3826,7 @@ public final class StoryItemSetContainerComponent: Component { let items = ContextController.Items(content: .list(itemList)) - let controller = ContextController( + let controller = makeContextController( presentationData: presentationData, source: .extracted(ListContextExtractedContentSource(contentView: sourceView)), items: .single(items), @@ -6761,7 +6761,7 @@ public final class StoryItemSetContainerComponent: Component { return .single(ContextController.Items(id: 0, content: .list(items), tip: tip, tipSignal: tipSignal)) } - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .bottom)), items: contextItems, gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .bottom)), items: contextItems, gesture: gesture) contextController.dismissed = { [weak self] in guard let self else { return @@ -7095,7 +7095,7 @@ public final class StoryItemSetContainerComponent: Component { return .single(ContextController.Items(id: 0, content: .list(items), tip: tip, tipSignal: tipSignal)) } - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .bottom)), items: contextItems, gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .bottom)), items: contextItems, gesture: gesture) contextController.dismissed = { [weak self] in guard let self else { return @@ -7526,7 +7526,7 @@ public final class StoryItemSetContainerComponent: Component { let contextItems = ContextController.Items(id: 0, content: .list(items), tip: tip, tipSignal: tipSignal) - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .bottom)), items: .single(contextItems), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .bottom)), items: .single(contextItems), gesture: gesture) contextController.dismissed = { [weak self] in guard let self else { return diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 619f13e4b1..dd7cfb187a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -538,7 +538,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let contextItems = ContextController.Items(content: .list(items)) - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .top)), items: .single(contextItems), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .top)), items: .single(contextItems), gesture: gesture) contextController.dismissed = { [weak view] in guard let view else { return @@ -4397,7 +4397,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) - let contextController = ContextController(presentationData: presentationData, source: .reference(ReferenceSource(controller: controller, sourceView: sourceView, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture, workaroundUseLegacyImplementation: false) + let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceSource(controller: controller, sourceView: sourceView, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture, workaroundUseLegacyImplementation: false) contextController.dismissed = { [weak self, weak view] in guard let self, let view else { return diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index 54490dfc18..23e3c7949a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -1458,7 +1458,7 @@ public final class StoryItemSetViewListComponent: Component { let contextItems = ContextController.Items(content: .list(items)) - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .bottom)), items: .single(contextItems), gesture: nil) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .bottom)), items: .single(contextItems), gesture: nil) sourceView.alpha = 0.5 contextController.dismissed = { [weak self, weak sourceView] in diff --git a/submodules/TelegramUI/Components/TranslateHeaderPanelComponent/Sources/ChatTranslationPanelNode.swift b/submodules/TelegramUI/Components/TranslateHeaderPanelComponent/Sources/ChatTranslationPanelNode.swift index 42b05c81df..07cc56bd14 100644 --- a/submodules/TelegramUI/Components/TranslateHeaderPanelComponent/Sources/ChatTranslationPanelNode.swift +++ b/submodules/TelegramUI/Components/TranslateHeaderPanelComponent/Sources/ChatTranslationPanelNode.swift @@ -422,7 +422,7 @@ final class ChatTranslationPanelNode: ASDisplayNode { } if let controller = self.controller() { - let contextController = ContextController(context: context, presentationData: presentationData, source: .reference(TranslationContextReferenceContentSource(controller: controller, sourceNode: node)), items: items, gesture: gesture) + let contextController = makeContextController(context: context, presentationData: presentationData, source: .reference(TranslationContextReferenceContentSource(controller: controller, sourceNode: node)), items: items, gesture: gesture) controller.presentInGlobalOverlay(contextController) } } diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 99b50385b0..8e51eb3151 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -43,6 +43,8 @@ import ContextMenuScreen import MetalEngine import RecaptchaEnterpriseSDK import NavigationBarImpl +import ContextUI +import ContextControllerImpl #if canImport(AppCenter) import AppCenter @@ -334,6 +336,62 @@ private func extractAccountManagerState(records: AccountRecordsView map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: source, items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) contextController.dismissed = { [weak self] in self?.canReadHistory.set(true) } @@ -5555,9 +5555,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if peerId == self.context.account.peerId { - PeerInfoScreenImpl.openSavedMessagesMoreMenu(context: self.context, sourceController: self, isViewingAsTopics: false, sourceView: sourceNode.view, gesture: gesture) + PeerInfoScreenImpl.openSavedMessagesMoreMenu(context: self.context, sourceController: self, isViewingAsTopics: false, sourceView: self.navigationBar?.navigationButtonContextContainer(sourceView: sourceNode.view) ?? sourceNode.view, gesture: gesture) } else if peerId.namespace == Namespaces.Peer.CloudUser { - self.openBotForumMoreMenu(sourceView: sourceNode.view, gesture: gesture) + self.openBotForumMoreMenu(sourceView: self.navigationBar?.navigationButtonContextContainer(sourceView: sourceNode.view) ?? sourceNode.view, gesture: gesture) } else { ChatListControllerImpl.openMoreMenu(context: self.context, peerId: peerId, sourceController: self, isViewingAsTopics: false, sourceView: sourceNode.view, gesture: gesture) } @@ -8094,7 +8094,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var defaultReplyMessageSubject: EngineMessageReplySubject? switch self.chatLocation { case .peer: - if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.isForum { + if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum), botInfo.flags.contains(.forumManagedByUser) { defaultThreadId = EngineMessage.newTopicThreadId } case let .replyThread(replyThreadMessage): diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 5fd702eb1d..3e831855b0 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2461,12 +2461,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } if let topBackgroundEdgeEffectNode { - var blurFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(100.0, listInsets.bottom + 24.0))) - blurFrame.origin.y = listInsets.bottom + 24.0 - blurFrame.height + let topExtent: CGFloat = 34.0 + var blurFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(100.0, listInsets.bottom + topExtent))) + blurFrame.origin.y = listInsets.bottom + topExtent - blurFrame.height transition.updateFrame(node: topBackgroundEdgeEffectNode, frame: blurFrame) topBackgroundEdgeEffectNode.update( rect: blurFrame, - edge: WallpaperEdgeEffectEdge(edge: .top, size: 70.0), + edge: WallpaperEdgeEffectEdge(edge: .top, size: 80.0), alpha: edgeEffectAlpha, blur: true, containerSize: wallpaperBounds.size, @@ -4640,7 +4641,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } var targetThreadId: Int64? - if self.chatLocation.threadId == nil, let user = self.chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.isForum { + if self.chatLocation.threadId == nil, let user = self.chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum), botInfo.flags.contains(.forumManagedByUser) { if let message = messages.first { switch message { case let .message(_, _, _, _, _, replyToMessageId, _, _, _, _): diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift index 6b89d6b1d3..81ead39eac 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift @@ -133,7 +133,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } ) diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index bd1b5f7ee7..ee684b7ffc 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -151,7 +151,7 @@ extension ChatControllerImpl { self.canReadHistory.set(false) - let controller = ContextController(presentationData: self.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, contentView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + let controller = makeContextController(presentationData: self.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, contentView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) controller.dismissed = { [weak self] in self?.canReadHistory.set(true) } @@ -348,7 +348,7 @@ extension ChatControllerImpl { self.canReadHistory.set(false) - let controller = ContextController(presentationData: self.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, contentView: sourceView)), items: .single(items), recognizer: nil, gesture: gesture) + let controller = makeContextController(presentationData: self.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, contentView: sourceView)), items: .single(items), recognizer: nil, gesture: gesture) controller.dismissed = { [weak self] in self?.canReadHistory.set(true) } diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 44097d730c..ed0023eb73 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -318,7 +318,7 @@ func chatHistoryEntriesForView( } var addBotForumHeader = false - if location.threadId == nil, let user = chatPeer as? TelegramUser, user.isForum, !entries.isEmpty, !view.holeEarlier, !view.isLoading { + if location.threadId == nil, let user = chatPeer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum), botInfo.flags.contains(.forumManagedByUser), !entries.isEmpty, !view.holeEarlier, !view.isLoading { addBotForumHeader = true outer: for i in (0 ..< entries.count).reversed() { switch entries[i] { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index ac69da0926..d0d1e1e30a 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -303,7 +303,7 @@ func floatingTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInte controller.openDeleteMonoforumPeer(peerId: EnginePeer.Id(threadId)) } ) - } else if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.isForum, chatPresentationInterfaceState.search == nil { + } else if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum), chatPresentationInterfaceState.search == nil { let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation return ChatFloatingTopicsPanel( context: context, @@ -311,7 +311,7 @@ func floatingTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInte strings: chatPresentationInterfaceState.strings, location: topicListDisplayModeOnTheSide ? .side : .top, peerId: peerId, - kind: .botForum, + kind: .botForum(forumManagedByUser: botInfo.flags.contains(.forumManagedByUser)), topicId: chatPresentationInterfaceState.chatLocation.threadId, controller: { [weak interfaceInteraction] in return interfaceInteraction?.chatController() diff --git a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift index 79e53b0adb..c9dae58747 100644 --- a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift @@ -380,7 +380,7 @@ final class ChatManagingBotTitlePanelNode: ChatTitleAccessoryPanelNode { } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: chatController, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: chatController, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) interfaceInteraction.presentController(contextController, nil) } diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index b0148af511..69fc59bb37 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -269,7 +269,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe 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, 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) + let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(content: .list([]))), gesture: gesture) presentInGlobalOverlay(contextController) } else { gesture?.cancel() diff --git a/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift index 5653241cf8..e404568775 100644 --- a/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift @@ -691,7 +691,7 @@ final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, Chat }) }))) - let controller = ContextController(presentationData: presentationData, source: .extracted(TagContextExtractedContentSource(controller: chatController, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + let controller = makeContextController(presentationData: presentationData, source: .extracted(TagContextExtractedContentSource(controller: chatController, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) interfaceInteraction.presentGlobalOverlayController(controller, nil) }) self.itemViews[itemId] = itemView diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index 7dbf6599c1..3f2a25e270 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -492,7 +492,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection } }))) - let contextController = ContextController(presentationData: presentationData, source: .reference(ContactContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(ContactContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.present(contextController, in: .window(.root)) } diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index 22a2c985e5..f72eb2c359 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -538,7 +538,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController } }))) - let contextController = ContextController(presentationData: presentationData, source: .reference(ContactContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(ContactContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.present(contextController, in: .window(.root)) return } diff --git a/submodules/TelegramUI/Sources/CreateGroupController.swift b/submodules/TelegramUI/Sources/CreateGroupController.swift index 05d3e8a256..b092e1c6a7 100644 --- a/submodules/TelegramUI/Sources/CreateGroupController.swift +++ b/submodules/TelegramUI/Sources/CreateGroupController.swift @@ -1232,7 +1232,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] let items: Signal = .single(ContextController.Items(content: .list(subItems))) let source: ContextContentSource = .reference(CreateGroupContextReferenceContentSource(sourceView: sourceNode.labelNode.view)) - let contextController = ContextController( + let contextController = makeContextController( presentationData: presentationData, source: source, items: items, diff --git a/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift index 80c2ecb74d..e66b38c535 100644 --- a/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift @@ -183,7 +183,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let controller = PeekController(presentationData: presentationData, content: content, sourceView: { + let controller = makePeekController(presentationData: presentationData, content: content, sourceView: { return (sourceView, sourceRect) }) /*controller.visibilityUpdated = { [weak self] visible in diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift index 369f90ac19..17b1fbd232 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -239,7 +239,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont }, present: { [weak self] content, sourceView, sourceRect in if let strongSelf = self { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let controller = PeekController(presentationData: presentationData, content: content, sourceView: { + let controller = makePeekController(presentationData: presentationData, content: content, sourceView: { return (sourceView, sourceRect) }) strongSelf.interfaceInteraction?.presentGlobalOverlayController(controller, nil) diff --git a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift index 3890be0c49..71d8698f71 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift @@ -257,7 +257,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { }, present: { [weak self] content, sourceView, sourceRect in if let strongSelf = self { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let controller = PeekController(presentationData: presentationData, content: content, sourceView: { + let controller = makePeekController(presentationData: presentationData, content: content, sourceView: { return (sourceView, sourceRect) }) strongSelf.interfaceInteraction?.presentGlobalOverlayController(controller, nil) diff --git a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift index ba99b13d69..6ad37b51a5 100644 --- a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift +++ b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift @@ -222,7 +222,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, ASScrollVie }, present: { [weak self] content, sourceView, sourceRect in if let strongSelf = self { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let controller = PeekController(presentationData: presentationData, content: content, sourceView: { + let controller = makePeekController(presentationData: presentationData, content: content, sourceView: { return (sourceView, sourceRect) }) controller.visibilityUpdated = { [weak self] visible in diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 399419c8b1..2b37c289bf 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -1264,7 +1264,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu return } - let contextController = ContextController(presentationData: presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture) self.getParentController()?.presentInGlobalOverlay(contextController) }) } diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift index 8ce36001b8..2c8eeada9e 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift @@ -1337,7 +1337,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { scheduledTooltip = change }) - let contextController = ContextController(presentationData: self.presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.rateButton.referenceNode, shouldBeDismissed: .single(false))), items: items, gesture: gesture) + let contextController = makeContextController(presentationData: self.presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.rateButton.referenceNode, shouldBeDismissed: .single(false))), items: items, gesture: gesture) contextController.dismissed = { [weak self] in if let scheduledTooltip, let self, let rate = self.currentRate { self.presentAudioRateTooltip(baseRate: rate, changeType: scheduledTooltip) diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h index ca13788e4d..925b95f9c6 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h @@ -26,6 +26,17 @@ typedef NS_OPTIONS(NSUInteger, UIResponderDisableAutomaticKeyboardHandling) { @end +@interface CALayerSpringParametersOverride : NSObject + +@end + +@interface CALayer (TelegramAddAnimation) + ++ (void)pushSpringParametersOverride:(CALayerSpringParametersOverride * _Nonnull)springParametersOverride; ++ (void)popSpringParametersOverride; + +@end + @interface UIView (Navigation) @property (nonatomic) bool disablesInteractiveTransitionGestureRecognizer; diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m index 1bc212adaa..09b23696a4 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m @@ -113,6 +113,70 @@ static bool notyfyingShiftState = false; @end +@implementation CALayerSpringParametersOverride + +@end + +static NSMutableArray *currentSpringParametersOverrideStack() { + static NSMutableArray *array = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + array = [[NSMutableArray alloc] init]; + }); + return array; +} + +@implementation CALayer (TelegramAddAnimation) + ++ (void)pushSpringParametersOverride:(CALayerSpringParametersOverride * _Nonnull)springParametersOverride { + if (springParametersOverride) { + [currentSpringParametersOverrideStack() addObject:springParametersOverride]; + } +} + ++ (void)popSpringParametersOverride { + if (currentSpringParametersOverrideStack().count != 0) { + [currentSpringParametersOverrideStack() removeLastObject]; + } +} + +- (void)_65087dc8_addAnimation:(CAAnimation *)anim forKey:(NSString *)key { + CAAnimation *updatedAnimation = anim; + if (currentSpringParametersOverrideStack().count != 0 && [anim isKindOfClass:[CASpringAnimation class]]) { + CALayerSpringParametersOverride *overrideData = [currentSpringParametersOverrideStack() lastObject]; + if (overrideData) { + bool isNativeGlass = false; + if (@available(iOS 26.0, *)) { + isNativeGlass = true; + } + if (isNativeGlass && ABS(anim.duration - 0.3832) <= 0.0001) { + } else if (ABS(anim.duration - 0.5) <= 0.0001) { + } else { + CABasicAnimation *sourceAnimation = (CABasicAnimation *)anim; + + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:sourceAnimation.keyPath]; + animation.fromValue = sourceAnimation.fromValue; + animation.toValue = sourceAnimation.toValue; + animation.byValue = sourceAnimation.byValue; + animation.additive = sourceAnimation.additive; + animation.duration = sourceAnimation.duration; + animation.timingFunction = [[CAMediaTimingFunction alloc] initWithControlPoints:0.380 :0.700 :0.125 :1.000]; + animation.removedOnCompletion = sourceAnimation.isRemovedOnCompletion; + animation.fillMode = sourceAnimation.fillMode; + animation.speed = sourceAnimation.speed; + animation.beginTime = sourceAnimation.beginTime; + animation.timeOffset = sourceAnimation.timeOffset; + animation.repeatCount = sourceAnimation.repeatCount; + animation.autoreverses = sourceAnimation.autoreverses; + updatedAnimation = animation; + } + } + } + [self _65087dc8_addAnimation:updatedAnimation forKey:key]; +} + +@end + @implementation UIScrollView (FrameRateRangeOverride) - (void)fixScrollDisplayLink { @@ -254,6 +318,8 @@ static void registerEffectViewOverrides(void) { [RuntimeUtils swizzleInstanceMethodOfClass:[CADisplayLink class] currentSelector:@selector(setPreferredFrameRateRange:) newSelector:@selector(_65087dc8_setPreferredFrameRateRange:)]; } + [RuntimeUtils swizzleInstanceMethodOfClass:[CALayer class] currentSelector:@selector(addAnimation:forKey:) newSelector:@selector(_65087dc8_addAnimation:forKey:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UIFocusSystem class] currentSelector:@selector(updateFocusIfNeeded) newSelector:@selector(_65087dc8_updateFocusIfNeeded)]; if (@available(iOS 26.0, *)) { diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift index e2b04a07d1..56c9b39ad1 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift @@ -158,7 +158,7 @@ final class WallpaperEdgeEffectNodeImpl: ASDisplayNode, WallpaperEdgeEffectNode transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerSize)) if blur { - let blurHeight: CGFloat = max(edge.size, bounds.height - 10.0) + let blurHeight: CGFloat = max(edge.size, bounds.height - 20.0) let blurFrame = CGRect(origin: CGPoint(x: 0.0, y: edge.edge == .bottom ? (bounds.height - blurHeight) : 0.0), size: CGSize(width: bounds.width, height: blurHeight)) let blurView: VariableBlurView if let current = self.blurView { diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index bc04472f13..f2bfddf4a5 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -3910,7 +3910,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return ContextController.Items(content: .list(items)) } - let contextController = ContextController(presentationData: presentationData, source: .reference(WebAppContextReferenceContentSource(controller: self, sourceView: view)), items: items, gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(WebAppContextReferenceContentSource(controller: self, sourceView: view)), items: items, gesture: gesture) self.presentInGlobalOverlay(contextController) }