diff --git a/submodules/AccountContext/Sources/MediaManager.swift b/submodules/AccountContext/Sources/MediaManager.swift index 7a24d417cf..528f8fa8ac 100644 --- a/submodules/AccountContext/Sources/MediaManager.swift +++ b/submodules/AccountContext/Sources/MediaManager.swift @@ -29,6 +29,7 @@ public protocol MediaManager: class { func filteredPlaylistState(accountId: AccountRecordId, playlistId: SharedMediaPlaylistId, itemId: SharedMediaPlaylistItemId, type: MediaManagerPlayerType) -> Signal func setOverlayVideoNode(_ node: OverlayMediaItemNode?) + func hasOverlayVideoNode(_ node: OverlayMediaItemNode) -> Bool func audioRecorder(beginWithTone: Bool, applicationBindings: TelegramApplicationBindings, beganWithTone: @escaping (Bool) -> Void) -> Signal } diff --git a/submodules/AccountContext/Sources/OverlayMediaItemNode.swift b/submodules/AccountContext/Sources/OverlayMediaItemNode.swift index 45033a0e8d..a635e5cca7 100644 --- a/submodules/AccountContext/Sources/OverlayMediaItemNode.swift +++ b/submodules/AccountContext/Sources/OverlayMediaItemNode.swift @@ -21,6 +21,9 @@ open class OverlayMediaItemNode: ASDisplayNode { open var unminimize: (() -> Void)? + public var manualExpandEmbed: (() -> Void)? + public var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)? + open var group: OverlayMediaItemNodeGroup? { return nil } diff --git a/submodules/Display/Source/Navigation/NavigationContainer.swift b/submodules/Display/Source/Navigation/NavigationContainer.swift index 260a043c25..51f9c7ddb9 100644 --- a/submodules/Display/Source/Navigation/NavigationContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationContainer.swift @@ -228,6 +228,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate { let velocity = recognizer.velocity(in: self.view).x if velocity > 1000 || navigationTransitionCoordinator.progress > 0.2 { + self.state.top?.value.viewWillLeaveNavigation() navigationTransitionCoordinator.animateCompletion(velocity, completion: { [weak self] in guard let strongSelf = self, let _ = strongSelf.state.layout, let _ = strongSelf.state.transition, let top = strongSelf.state.top else { return @@ -399,6 +400,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate { }) } + fromValue.value.viewWillLeaveNavigation() fromValue.value.viewWillDisappear(true) toValue.value.viewWillAppear(true) toValue.value.setIgnoreAppearanceMethodInvocations(true) @@ -464,6 +466,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate { }) } else { if let fromValue = fromValue { + fromValue.value.viewWillLeaveNavigation() fromValue.value.viewWillDisappear(false) fromValue.value.setIgnoreAppearanceMethodInvocations(true) fromValue.value.displayNode.removeFromSupernode() diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 217c0d133a..f94da53fb4 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -117,6 +117,7 @@ open class NavigationBar: ASDisplayNode { public var userInfo: Any? public var makeCustomTransitionNode: ((NavigationBar, Bool) -> CustomNavigationTransitionNode?)? + public var allowsCustomTransition: (() -> Bool)? private var collapsed: Bool { get { diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 2fee5bd184..a563e65e05 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -561,6 +561,9 @@ public enum TabBarItemContextActionType { super.viewDidDisappear(animated) } + open func viewWillLeaveNavigation() { + } + open override func viewDidAppear(_ animated: Bool) { self.activeInputView = nil diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 503adf38e0..3d3a40ee5a 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -290,6 +290,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var playbackCompleted: (() -> Void)? + private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)? + init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.presentationData = presentationData @@ -423,6 +425,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + if let _ = self.customUnembedWhenPortrait, layout.size.width < layout.size.height { + self.expandIntoCustomPiP() + } + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) var dismiss = false @@ -888,6 +894,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } if let node = node.0 as? OverlayMediaItemNode { + self.customUnembedWhenPortrait = node.customUnembedWhenPortrait + node.customUnembedWhenPortrait = nil + } + + if let node = node.0 as? OverlayMediaItemNode, self.context.sharedContext.mediaManager.hasOverlayVideoNode(node) { var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) @@ -960,10 +971,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) } - videoNode.allowsGroupOpacity = true - videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak videoNode] _ in - videoNode?.allowsGroupOpacity = false - }) + if surfaceCopyView.superview != nil { + videoNode.allowsGroupOpacity = true + videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak videoNode] _ in + videoNode?.allowsGroupOpacity = false + }) + } videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) transformedFrame.origin = CGPoint() @@ -1272,9 +1285,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - @objc func pictureInPictureButtonPressed() { - if let item = self.item, let videoNode = self.videoNode { - + private func expandIntoCustomPiP() { + if let item = self.item, let videoNode = self.videoNode, let customUnembedWhenPortrait = customUnembedWhenPortrait { + self.customUnembedWhenPortrait = nil videoNode.setContinuePlayingWithoutSoundOnLostAudioSession(false) let context = self.context @@ -1306,9 +1319,77 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode { return GalleryTransitionArguments(transitionNode: (overlayNode, overlayNode.bounds, { [weak overlayNode] in return (overlayNode?.view.snapshotContentTree(), nil) - }), addToTransitionSurface: { [weak overlaySupernode, weak overlayNode] view in - overlaySupernode?.view.addSubview(view) - overlayNode?.canAttachContent = false + }), addToTransitionSurface: { [weak context, weak overlaySupernode, weak overlayNode] view in + guard let context = context, let overlayNode = overlayNode else { + return + } + if context.sharedContext.mediaManager.hasOverlayVideoNode(overlayNode) { + overlaySupernode?.view.addSubview(view) + } + overlayNode.canAttachContent = false + }) + } else if let info = context.sharedContext.mediaManager.galleryHiddenMediaManager.findTarget(messageId: id, media: media) { + return GalleryTransitionArguments(transitionNode: (info.1, info.1.bounds, { + return info.2() + }), addToTransitionSurface: info.0) + } + return nil + })) + case .webPage: + break + } + } + if customUnembedWhenPortrait(overlayNode) { + self.beginCustomDismiss() + self.statusNode.isHidden = true + self.animateOut(toOverlay: overlayNode, completion: { [weak self] in + self?.completeCustomDismiss() + }) + } + } + } + + @objc func pictureInPictureButtonPressed() { + if let item = self.item, let videoNode = self.videoNode { + videoNode.setContinuePlayingWithoutSoundOnLostAudioSession(false) + + let context = self.context + let baseNavigationController = self.baseNavigationController() + let mediaManager = self.context.sharedContext.mediaManager + var expandImpl: (() -> Void)? + let overlayNode = OverlayUniversalVideoNode(postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, expand: { + expandImpl?() + }, close: { [weak mediaManager] in + mediaManager?.setOverlayVideoNode(nil) + }) + expandImpl = { [weak overlayNode] in + guard let contentInfo = item.contentInfo, let overlayNode = overlayNode else { + return + } + + switch contentInfo { + case let .message(message): + let gallery = GalleryController(context: context, source: .peerMessagesAtId(message.id), replaceRootController: { controller, ready in + if let baseNavigationController = baseNavigationController { + baseNavigationController.replaceTopController(controller, animated: false, ready: ready) + } + }, baseNavigationController: baseNavigationController) + gallery.temporaryDoNotWaitForReady = true + + baseNavigationController?.view.endEditing(true) + + (baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { [weak overlayNode] id, media in + if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode { + return GalleryTransitionArguments(transitionNode: (overlayNode, overlayNode.bounds, { [weak overlayNode] in + return (overlayNode?.view.snapshotContentTree(), nil) + }), addToTransitionSurface: { [weak context, weak overlaySupernode, weak overlayNode] view in + guard let context = context, let overlayNode = overlayNode else { + return + } + if context.sharedContext.mediaManager.hasOverlayVideoNode(overlayNode) { + overlaySupernode?.view.addSubview(view) + } + overlayNode.canAttachContent = false }) } else if let info = context.sharedContext.mediaManager.galleryHiddenMediaManager.findTarget(messageId: id, media: media) { return GalleryTransitionArguments(transitionNode: (info.1, info.1.bounds, { diff --git a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift index 70e63b132c..b1258e72da 100644 --- a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift +++ b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift @@ -45,9 +45,9 @@ public extension TabBarControllerTheme { } public extension NavigationBarTheme { - convenience init(rootControllerTheme: PresentationTheme, hideBackground: Bool = false) { + convenience init(rootControllerTheme: PresentationTheme, hideBackground: Bool = false, hideBadge: Bool = false) { let theme = rootControllerTheme.rootController.navigationBar - self.init(buttonColor: theme.buttonColor, disabledButtonColor: theme.disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.backgroundColor, separatorColor: hideBackground ? .clear : theme.separatorColor, badgeBackgroundColor: theme.badgeBackgroundColor, badgeStrokeColor: hideBackground ? .clear : theme.badgeStrokeColor, badgeTextColor: theme.badgeTextColor) + self.init(buttonColor: theme.buttonColor, disabledButtonColor: theme.disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.backgroundColor, separatorColor: hideBackground ? .clear : theme.separatorColor, badgeBackgroundColor: hideBadge ? .clear : theme.badgeBackgroundColor, badgeStrokeColor: hideBadge ? .clear : theme.badgeStrokeColor, badgeTextColor: hideBadge ? .clear : theme.badgeTextColor) } } @@ -62,8 +62,8 @@ public extension NavigationBarPresentationData { self.init(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings)) } - convenience init(presentationData: PresentationData, hideBackground: Bool) { - self.init(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme, hideBackground: hideBackground), strings: NavigationBarStrings(presentationStrings: presentationData.strings)) + convenience init(presentationData: PresentationData, hideBackground: Bool, hideBadge: Bool) { + self.init(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme, hideBackground: hideBackground, hideBadge: hideBadge), strings: NavigationBarStrings(presentationStrings: presentationData.strings)) } convenience init(presentationTheme: PresentationTheme, presentationStrings: PresentationStrings) { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 79713d4e9d..dc3cdd03f3 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -322,6 +322,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private let peekTimerDisposable = MetaDisposable() private var hasEmbeddedTitleContent = false + private var isEmbeddedTitleContentHidden = false public override var customData: Any? { return self.chatLocation @@ -375,7 +376,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .inline: navigationBarPresentationData = nil default: - navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: true) + navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: true, hideBadge: false) } super.init(context: context, navigationBarPresentationData: navigationBarPresentationData, mediaAccessoryPanelVisibility: mediaAccessoryPanelVisibility, locationBroadcastPanelSource: locationBroadcastPanelSource) @@ -2059,6 +2060,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case let .peer(peerId) = chatLocation, peerId != context.account.peerId, subject != .scheduledMessages { self.navigationBar?.userInfo = PeerInfoNavigationSourceTag(peerId: peerId) } + self.navigationBar?.allowsCustomTransition = { [weak self] in + guard let strongSelf = self else { + return false + } + return !strongSelf.chatDisplayNode.hasEmbeddedTitleContent + } self.chatTitleView = ChatTitleView(account: self.context.account, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder) self.navigationItem.titleView = self.chatTitleView @@ -2799,9 +2806,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let navigationBarTheme: NavigationBarTheme if self.hasEmbeddedTitleContent { - navigationBarTheme = NavigationBarTheme(rootControllerTheme: defaultDarkPresentationTheme, hideBackground: true) + navigationBarTheme = NavigationBarTheme(rootControllerTheme: defaultDarkPresentationTheme, hideBackground: true, hideBadge: true) } else { - navigationBarTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme, hideBackground: true) + navigationBarTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme, hideBackground: true, hideBadge: false) } self.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) @@ -4750,10 +4757,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - self.chatDisplayNode.updateHasEmbeddedTitleContent = { [weak self] hasEmbeddedTitleContent in + self.chatDisplayNode.updateHasEmbeddedTitleContent = { [weak self] in guard let strongSelf = self else { return } + + let hasEmbeddedTitleContent = strongSelf.chatDisplayNode.hasEmbeddedTitleContent + let isEmbeddedTitleContentHidden = strongSelf.chatDisplayNode.isEmbeddedTitleContentHidden + if strongSelf.hasEmbeddedTitleContent != hasEmbeddedTitleContent { strongSelf.hasEmbeddedTitleContent = hasEmbeddedTitleContent @@ -4774,6 +4785,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.updateNavigationBarPresentation() } + + if strongSelf.isEmbeddedTitleContentHidden != isEmbeddedTitleContentHidden { + strongSelf.isEmbeddedTitleContentHidden = isEmbeddedTitleContentHidden + + if let navigationBar = strongSelf.navigationBar { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + transition.updateAlpha(node: navigationBar, alpha: isEmbeddedTitleContentHidden ? 0.0 : 1.0) + } + } } self.interfaceInteraction = interfaceInteraction @@ -5168,6 +5188,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } + override public func viewWillLeaveNavigation() { + self.chatDisplayNode.willNavigateAway() + } + override public func inFocusUpdated(isInFocus: Bool) { self.chatDisplayNode.inFocusUpdated(isInFocus: isInFocus) } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index b4abb27535..18d6e9d6f3 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -69,6 +69,9 @@ private final class ChatEmbeddedTitleContentNode: ASDisplayNode { private let context: AccountContext private let backgroundNode: ASDisplayNode private let videoNode: OverlayUniversalVideoNode + private let disableInternalAnimationIn: Bool + private let isUIHiddenUpdated: () -> Void + private let unembedWhenPortrait: (OverlayMediaItemNode) -> Bool private var validLayout: (CGSize, CGFloat, CGFloat)? @@ -78,9 +81,14 @@ private final class ChatEmbeddedTitleContentNode: ASDisplayNode { private(set) var interactiveExtension: CGFloat = 0.0 private var freezeInteractiveExtension = false - init(context: AccountContext, videoNode: OverlayUniversalVideoNode, interactiveExtensionUpdated: @escaping (ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void) { + private(set) var isUIHidden: Bool = false + + init(context: AccountContext, videoNode: OverlayUniversalVideoNode, disableInternalAnimationIn: Bool, interactiveExtensionUpdated: @escaping (ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void, isUIHiddenUpdated: @escaping () -> Void, unembedWhenPortrait: @escaping (OverlayMediaItemNode) -> Bool) { self.dismissed = dismissed self.interactiveExtensionUpdated = interactiveExtensionUpdated + self.isUIHiddenUpdated = isUIHiddenUpdated + self.unembedWhenPortrait = unembedWhenPortrait + self.disableInternalAnimationIn = disableInternalAnimationIn self.context = context @@ -96,6 +104,14 @@ private final class ChatEmbeddedTitleContentNode: ASDisplayNode { self.addSubnode(self.backgroundNode) self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + + self.videoNode.controlsAreShowingUpdated = { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.isUIHidden = !value + strongSelf.isUIHiddenUpdated() + } } @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { @@ -116,7 +132,7 @@ private final class ChatEmbeddedTitleContentNode: ASDisplayNode { if translation.y > 80.0 { self.freezeInteractiveExtension = true - self.videoNode.customExpand?() + self.expandIntoPiP() } else { self.interactiveExtension = max(0.0, offset) self.interactiveExtensionUpdated(.immediate) @@ -146,72 +162,63 @@ private final class ChatEmbeddedTitleContentNode: ASDisplayNode { let sourceFrame = self.videoNode.view.convert(self.videoNode.bounds, to: transitionSurface.view) let targetFrame = self.view.convert(videoFrame, to: transitionSurface.view) - self.context.sharedContext.mediaManager.setOverlayVideoNode(nil) - transitionSurface.addSubnode(self.videoNode) - - let navigationBarCopy = navigationBar?.view.snapshotView(afterScreenUpdates: true) - let navigationBarContainer = UIView() - navigationBarContainer.frame = targetFrame - navigationBarContainer.clipsToBounds = true - transitionSurface.view.addSubview(navigationBarContainer) - - navigationBarContainer.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - - if let navigationBar = navigationBar, let navigationBarCopy = navigationBarCopy { - let navigationFrame = navigationBar.view.convert(navigationBar.bounds, to: transitionSurface.view) - let navigationSourceFrame = navigationFrame.offsetBy(dx: -sourceFrame.minX, dy: -sourceFrame.minY) - let navigationTargetFrame = navigationFrame.offsetBy(dx: -targetFrame.minX, dy: -targetFrame.minY) - navigationBarCopy.frame = navigationTargetFrame - navigationBarContainer.addSubview(navigationBarCopy) + var navigationBarCopy: UIView? + var navigationBarContainer: UIView? + var nodeTransition = transition + if self.disableInternalAnimationIn { + nodeTransition = .immediate + } else { + self.context.sharedContext.mediaManager.setOverlayVideoNode(nil) + transitionSurface.addSubnode(self.videoNode) - navigationBarCopy.layer.animateFrame(from: navigationSourceFrame, to: navigationTargetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - navigationBarCopy.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + navigationBarCopy = navigationBar?.view.snapshotView(afterScreenUpdates: true) + let navigationBarContainerValue = UIView() + navigationBarContainer = navigationBarContainerValue + navigationBarContainerValue.frame = targetFrame + navigationBarContainerValue.clipsToBounds = true + transitionSurface.view.addSubview(navigationBarContainerValue) } - self.videoNode.updateRoundCorners(false, transition: .animated(duration: 0.25, curve: .spring)) - self.videoNode.showControls() + if !self.disableInternalAnimationIn { + navigationBarContainer?.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + } - self.videoNode.updateLayout(targetFrame.size, transition: .animated(duration: 0.25, curve: .spring)) - self.videoNode.frame = targetFrame - self.videoNode.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in - guard let strongSelf = self else { - return - } - navigationBarContainer.removeFromSuperview() - strongSelf.addSubnode(strongSelf.videoNode) - if let (size, topInset, interactiveExtension) = strongSelf.validLayout { - strongSelf.updateLayout(size: size, topInset: topInset, interactiveExtension: interactiveExtension, transition: .immediate, transitionSurface: nil, navigationBar: nil) - } - }) - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - - self.videoNode.customExpand = { [weak self] in - guard let strongSelf = self else { - return - } - - let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) - - strongSelf.videoNode.customExpand = nil - strongSelf.videoNode.customClose = nil - - let previousFrame = strongSelf.videoNode.frame - strongSelf.context.sharedContext.mediaManager.setOverlayVideoNode(strongSelf.videoNode) - strongSelf.videoNode.updateRoundCorners(true, transition: transition) - - if let targetSuperview = strongSelf.videoNode.view.superview { - let sourceFrame = strongSelf.view.convert(previousFrame, to: targetSuperview) - let targetFrame = strongSelf.videoNode.frame - strongSelf.videoNode.frame = sourceFrame - strongSelf.videoNode.updateLayout(sourceFrame.size, transition: .immediate) + if !self.disableInternalAnimationIn { + if let navigationBar = navigationBar, let navigationBarCopy = navigationBarCopy { + let navigationFrame = navigationBar.view.convert(navigationBar.bounds, to: transitionSurface.view) + let navigationSourceFrame = navigationFrame.offsetBy(dx: -sourceFrame.minX, dy: -sourceFrame.minY) + let navigationTargetFrame = navigationFrame.offsetBy(dx: -targetFrame.minX, dy: -targetFrame.minY) + navigationBarCopy.frame = navigationTargetFrame + navigationBarContainer?.addSubview(navigationBarCopy) - transition.updateFrame(node: strongSelf.videoNode, frame: targetFrame) - strongSelf.videoNode.updateLayout(targetFrame.size, transition: transition) + navigationBarCopy.layer.animateFrame(from: navigationSourceFrame, to: navigationTargetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + navigationBarCopy.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } - - strongSelf.dismissed() } + self.videoNode.updateRoundCorners(false, transition: nodeTransition) + if !self.disableInternalAnimationIn { + self.videoNode.showControls() + } + + self.videoNode.updateLayout(targetFrame.size, transition: nodeTransition) + self.videoNode.frame = targetFrame + if self.disableInternalAnimationIn { + self.addSubnode(self.videoNode) + } else { + self.videoNode.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + navigationBarContainer?.removeFromSuperview() + strongSelf.addSubnode(strongSelf.videoNode) + if let (size, topInset, interactiveExtension) = strongSelf.validLayout { + strongSelf.updateLayout(size: size, topInset: topInset, interactiveExtension: interactiveExtension, transition: .immediate, transitionSurface: nil, navigationBar: nil) + } + }) + self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + self.videoNode.customClose = { [weak self] in guard let strongSelf = self else { return @@ -227,6 +234,39 @@ private final class ChatEmbeddedTitleContentNode: ASDisplayNode { transition.updateFrame(node: self.videoNode, frame: videoFrame) } } + + func expand(intoLandscape: Bool) { + if intoLandscape { + let unembedWhenPortrait = self.unembedWhenPortrait + self.videoNode.customUnembedWhenPortrait = { videoNode in + unembedWhenPortrait(videoNode) + } + } + self.videoNode.expand() + } + + func expandIntoPiP() { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) + + self.videoNode.customExpand = nil + self.videoNode.customClose = nil + + let previousFrame = self.videoNode.frame + self.context.sharedContext.mediaManager.setOverlayVideoNode(self.videoNode) + self.videoNode.updateRoundCorners(true, transition: transition) + + if let targetSuperview = self.videoNode.view.superview { + let sourceFrame = self.view.convert(previousFrame, to: targetSuperview) + let targetFrame = self.videoNode.frame + self.videoNode.frame = sourceFrame + self.videoNode.updateLayout(sourceFrame.size, transition: .immediate) + + transition.updateFrame(node: self.videoNode, frame: targetFrame) + self.videoNode.updateLayout(targetFrame.size, transition: transition) + } + + self.dismissed() + } } enum ChatEmbeddedTitlePeekContent: Equatable { @@ -387,6 +427,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var embeddedTitlePeekContent: ChatEmbeddedTitlePeekContent = .none private var embeddedTitleContentNode: ChatEmbeddedTitleContentNode? private var dismissedEmbeddedTitleContentNode: ChatEmbeddedTitleContentNode? + var hasEmbeddedTitleContent: Bool { + return self.embeddedTitleContentNode != nil + } init(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: MediaAutoDownloadSettings, navigationBar: NavigationBar?, controller: ChatControllerImpl?) { self.context = context @@ -892,6 +935,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let statusBarHeight = layout.insets(options: [.statusBar]).top + if let embeddedTitleContentNode = self.embeddedTitleContentNode, embeddedTitleContentNode.supernode != nil { + if layout.size.width > layout.size.height { + self.embeddedTitleContentNode = nil + self.dismissedEmbeddedTitleContentNode = embeddedTitleContentNode + embeddedTitleContentNode.expand(intoLandscape: true) + self.updateHasEmbeddedTitleContent?() + } + } + if let embeddedTitleContentNode = self.embeddedTitleContentNode { let embeddedSize = CGSize(width: layout.size.width, height: min(400.0, embeddedTitleContentNode.calculateHeight(width: layout.size.width)) + statusBarHeight + embeddedTitleContentNode.interactiveExtension) if embeddedTitleContentNode.supernode == nil { @@ -2648,7 +2700,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - var updateHasEmbeddedTitleContent: ((Bool) -> Void)? + var isEmbeddedTitleContentHidden: Bool { + if let embeddedTitleContentNode = self.embeddedTitleContentNode { + return embeddedTitleContentNode.isUIHidden + } else { + return false + } + } + + var updateHasEmbeddedTitleContent: (() -> Void)? func acceptEmbeddedTitlePeekContent(content: NavigationControllerDropContent) -> Bool { guard let (_, navigationHeight) = self.validLayout else { @@ -2658,7 +2718,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { return false } if let item = content.item as? VideoNavigationControllerDropContentItem, let itemNode = item.itemNode as? OverlayUniversalVideoNode { - let embeddedTitleContentNode = ChatEmbeddedTitleContentNode(context: self.context, videoNode: itemNode, interactiveExtensionUpdated: { [weak self] transition in + let embeddedTitleContentNode = ChatEmbeddedTitleContentNode(context: self.context, videoNode: itemNode, disableInternalAnimationIn: false, interactiveExtensionUpdated: { [weak self] transition in guard let strongSelf = self else { return } @@ -2671,12 +2731,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { strongSelf.embeddedTitleContentNode = nil strongSelf.dismissedEmbeddedTitleContentNode = embeddedTitleContentNode strongSelf.requestLayout(.animated(duration: 0.25, curve: .spring)) - strongSelf.updateHasEmbeddedTitleContent?(false) + strongSelf.updateHasEmbeddedTitleContent?() } + }, isUIHiddenUpdated: { [weak self] in + self?.updateHasEmbeddedTitleContent?() + }, unembedWhenPortrait: { [weak self] itemNode in + guard let strongSelf = self, let itemNode = itemNode as? OverlayUniversalVideoNode else { + return false + } + strongSelf.unembedWhenPortrait(contentNode: itemNode) + return true }) self.embeddedTitleContentNode = embeddedTitleContentNode self.embeddedTitlePeekContent = .none - self.updateHasEmbeddedTitleContent?(true) + self.updateHasEmbeddedTitleContent?() DispatchQueue.main.async { self.requestLayout(.animated(duration: 0.25, curve: .spring)) } @@ -2685,4 +2753,46 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } return false } + + private func unembedWhenPortrait(contentNode: OverlayUniversalVideoNode) { + let embeddedTitleContentNode = ChatEmbeddedTitleContentNode(context: self.context, videoNode: contentNode, disableInternalAnimationIn: true, interactiveExtensionUpdated: { [weak self] transition in + guard let strongSelf = self else { + return + } + strongSelf.requestLayout(transition) + }, dismissed: { [weak self] in + guard let strongSelf = self else { + return + } + if let embeddedTitleContentNode = strongSelf.embeddedTitleContentNode { + strongSelf.embeddedTitleContentNode = nil + strongSelf.dismissedEmbeddedTitleContentNode = embeddedTitleContentNode + strongSelf.requestLayout(.animated(duration: 0.25, curve: .spring)) + strongSelf.updateHasEmbeddedTitleContent?() + } + }, isUIHiddenUpdated: { [weak self] in + self?.updateHasEmbeddedTitleContent?() + }, unembedWhenPortrait: { [weak self] itemNode in + guard let strongSelf = self, let itemNode = itemNode as? OverlayUniversalVideoNode else { + return false + } + strongSelf.unembedWhenPortrait(contentNode: itemNode) + return true + }) + + self.embeddedTitleContentNode = embeddedTitleContentNode + self.embeddedTitlePeekContent = .none + self.updateHasEmbeddedTitleContent?() + self.requestLayout(.immediate) + } + + func willNavigateAway() { + if let embeddedTitleContentNode = self.embeddedTitleContentNode { + self.embeddedTitleContentNode = nil + self.dismissedEmbeddedTitleContentNode = embeddedTitleContentNode + embeddedTitleContentNode.expandIntoPiP() + self.requestLayout(.animated(duration: 0.25, curve: .spring)) + self.updateHasEmbeddedTitleContent?() + } + } } diff --git a/submodules/TelegramUI/Sources/MediaManager.swift b/submodules/TelegramUI/Sources/MediaManager.swift index 2db3ce7136..7644847d6c 100644 --- a/submodules/TelegramUI/Sources/MediaManager.swift +++ b/submodules/TelegramUI/Sources/MediaManager.swift @@ -616,4 +616,8 @@ public final class MediaManagerImpl: NSObject, MediaManager { self.overlayMediaManager.controller?.addNode(node, customTransition: true) } } + + public func hasOverlayVideoNode(_ node: OverlayMediaItemNode) -> Bool { + return self.currentOverlayVideoNode === node + } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 05a01f763d..aca33637fd 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -4345,6 +4345,9 @@ public final class PeerInfoScreen: ViewController { if other.contentNode != nil { return nil } + if let allowsCustomTransition = other.allowsCustomTransition, !allowsCustomTransition() { + return nil + } if let tag = other.userInfo as? PeerInfoNavigationSourceTag, tag.peerId == peerId { return PeerInfoNavigationTransitionNode(screenNode: strongSelf.controllerNode, presentationData: strongSelf.presentationData, headerNode: strongSelf.controllerNode.headerNode) } diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index ca6aa965e9..5fe146c046 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -287,10 +287,20 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent applyLayout() } - self.imageNode.frame = CGRect(origin: CGPoint(), size: size) - self.playerNode.frame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0) + transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size)) + let fromFrame = self.playerNode.frame + let toFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0) + if case let .animated(duration, curve) = transition, fromFrame != toFrame, !fromFrame.width.isZero, !fromFrame.height.isZero, !toFrame.width.isZero, !toFrame.height.isZero { + self.playerNode.frame = toFrame + transition.animatePosition(node: self.playerNode, from: CGPoint(x: fromFrame.center.x - toFrame.center.x, y: fromFrame.center.y - toFrame.center.y)) + + let transform = CATransform3DScale(CATransform3DIdentity, fromFrame.width / toFrame.width, fromFrame.height / toFrame.height, 1.0) + self.playerNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration) + } else { + transition.updateFrame(node: self.playerNode, frame: toFrame) + } if let thumbnailNode = self.thumbnailNode { - thumbnailNode.frame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0) + transition.updateFrame(node: thumbnailNode, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0)) } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift b/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift index 6265ef0b31..ff10e06372 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift @@ -33,17 +33,19 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode { private let defaultExpand: () -> Void public var customExpand: (() -> Void)? public var customClose: (() -> Void)? + public var controlsAreShowingUpdated: ((Bool) -> Void)? public init(postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, expand: @escaping () -> Void, close: @escaping () -> Void) { self.content = content self.defaultExpand = expand var expandImpl: (() -> Void)? + var controlsAreShowingUpdatedImpl: ((Bool) -> Void)? var unminimizeImpl: (() -> Void)? var togglePlayPauseImpl: (() -> Void)? var closeImpl: (() -> Void)? - let decoration = OverlayVideoDecoration(unminimize: { + let decoration = OverlayVideoDecoration(contentDimensions: content.dimensions, unminimize: { unminimizeImpl?() }, togglePlayPause: { togglePlayPauseImpl?() @@ -51,6 +53,8 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode { expandImpl?() }, close: { closeImpl?() + }, controlsAreShowingUpdated: { value in + controlsAreShowingUpdatedImpl?(value) }) self.videoNode = UniversalVideoNode(postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .overlay) self.decoration = decoration @@ -58,14 +62,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode { super.init() expandImpl = { [weak self] in - guard let strongSelf = self else { - return - } - if let customExpand = strongSelf.customExpand { - customExpand() - } else { - strongSelf.defaultExpand() - } + self?.expand() } unminimizeImpl = { [weak self] in @@ -90,6 +87,10 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode { strongSelf.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) } } + + controlsAreShowingUpdatedImpl = { [weak self] value in + self?.controlsAreShowingUpdated?(value) + } self.clipsToBounds = true self.cornerRadius = 4.0 @@ -104,7 +105,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode { if previous != value { if !value { strongSelf.dismiss() - close() + closeImpl?() } } } @@ -155,4 +156,12 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode { public func showControls() { self.decoration.showControls() } + + public func expand() { + if let customExpand = self.customExpand { + customExpand() + } else { + self.defaultExpand() + } + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift b/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift index eb6ed63083..4cacd66bc7 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift @@ -26,11 +26,14 @@ private func setupArrowFrame(size: CGSize, edge: OverlayMediaItemMinimizationEdg private let backgroundImage = UIImage(bundleImageName: "Chat/Message/OverlayPlainVideoShadow")?.precomposed().resizableImage(withCapInsets: UIEdgeInsets(top: 22.0, left: 25.0, bottom: 26.0, right: 25.0), resizingMode: .stretch) final class OverlayVideoDecoration: UniversalVideoDecoration { + private let contentDimensions: CGSize + let backgroundNode: ASDisplayNode? let contentContainerNode: ASDisplayNode let foregroundNode: ASDisplayNode? private let unminimize: () -> Void + private let controlsAreShowingUpdated: (Bool) -> Void private let shadowNode: ASImageNode private let foregroundContainerNode: ASDisplayNode @@ -46,8 +49,11 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { private var validLayoutSize: CGSize? - init(unminimize: @escaping () -> Void, togglePlayPause: @escaping () -> Void, expand: @escaping () -> Void, close: @escaping () -> Void) { + init(contentDimensions: CGSize, unminimize: @escaping () -> Void, togglePlayPause: @escaping () -> Void, expand: @escaping () -> Void, close: @escaping () -> Void, controlsAreShowingUpdated: @escaping (Bool) -> Void) { + self.contentDimensions = contentDimensions + self.unminimize = unminimize + self.controlsAreShowingUpdated = controlsAreShowingUpdated self.shadowNode = ASImageNode() self.shadowNode.image = backgroundImage @@ -78,6 +84,14 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { self.statusDisposable.dispose() } + private func frameForContent(size: CGSize) -> CGRect { + if !self.contentDimensions.width.isZero && !self.contentDimensions.height.isZero { + let fittedSize = self.contentDimensions.aspectFittedWithOverflow(size, leeway: 10.0) + return CGRect(origin: CGPoint(x: floor(size.width - fittedSize.width) / 2.0, y: floor(size.height - fittedSize.height) / 2.0), size: fittedSize) + } + return CGRect(origin: CGPoint(), size: size) + } + func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) { if self.contentNode !== contentNode { let previous = self.contentNode @@ -93,7 +107,7 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) + contentNode.frame = self.frameForContent(size: validLayoutSize) contentNode.updateLayout(size: validLayoutSize, transition: .immediate) } } @@ -107,6 +121,8 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { self.validLayoutSize = size + let contentFrame = self.frameForContent(size: size) + let shadowInsets = UIEdgeInsets(top: 2.0, left: 3.0, bottom: 4.0, right: 3.0) transition.updateFrame(node: self.shadowNode, frame: CGRect(origin: CGPoint(x: -shadowInsets.left, y: -shadowInsets.top), size: CGSize(width: size.width + shadowInsets.left + shadowInsets.right, height: size.height + shadowInsets.top + shadowInsets.bottom))) @@ -129,8 +145,8 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - progressSize.width) / 2.0), y: floorToScreenPixels((size.height - progressSize.height) / 2.0)), size: progressSize)) if let contentNode = self.contentNode { - transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + transition.updateFrame(node: contentNode, frame: contentFrame) + contentNode.updateLayout(size: contentFrame.size, transition: transition) } } @@ -141,9 +157,11 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { if self.controlsNode.alpha.isZero { self.controlsNode.alpha = 1.0 self.controlsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.controlsAreShowingUpdated(true) } else { self.controlsNode.alpha = 0.0 self.controlsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + self.controlsAreShowingUpdated(false) } } } @@ -152,6 +170,7 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { if self.controlsNode.alpha.isZero { self.controlsNode.alpha = 1.0 self.controlsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.controlsAreShowingUpdated(true) } }