diff --git a/submodules/AccountContext/Sources/OverlayMediaManager.swift b/submodules/AccountContext/Sources/OverlayMediaManager.swift index 15edac8a7d..45d7eec061 100644 --- a/submodules/AccountContext/Sources/OverlayMediaManager.swift +++ b/submodules/AccountContext/Sources/OverlayMediaManager.swift @@ -1,7 +1,24 @@ import Foundation +import UIKit import Display +public final class OverlayMediaControllerEmbeddingItem { + public let position: CGPoint + public let itemNode: OverlayMediaItemNode + + public init( + position: CGPoint, + itemNode: OverlayMediaItemNode + ) { + self.position = position + self.itemNode = itemNode + } +} + public protocol OverlayMediaController: class { + var updatePossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem?) -> Void)? { get set } + var embedPossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem) -> Bool)? { get set } + var hasNodes: Bool { get } func addNode(_ node: OverlayMediaItemNode, customTransition: Bool) func removeNode(_ node: OverlayMediaItemNode, customTransition: Bool) @@ -10,10 +27,21 @@ public protocol OverlayMediaController: class { public final class OverlayMediaManager { public var controller: (OverlayMediaController & ViewController)? + public var updatePossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem?) -> Void)? + public var embedPossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem) -> Bool)? + public init() { } public func attachOverlayMediaController(_ controller: OverlayMediaController & ViewController) { self.controller = controller + + controller.updatePossibleEmbeddingItem = { [weak self] item in + self?.updatePossibleEmbeddingItem?(item) + } + + controller.embedPossibleEmbeddingItem = { [weak self] item in + return self?.embedPossibleEmbeddingItem?(item) ?? false + } } } diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index 101cdbf2d7..26a12835c0 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -102,6 +102,19 @@ private final class NavigationControllerNode: ASDisplayNode { } } +public protocol NavigationControllerDropContentItem: class { +} + +public final class NavigationControllerDropContent { + public let position: CGPoint + public let item: NavigationControllerDropContentItem + + public init(position: CGPoint, item: NavigationControllerDropContentItem) { + self.position = position + self.item = item + } +} + open class NavigationController: UINavigationController, ContainableController, UIGestureRecognizerDelegate { public var isOpaqueWhenInOverlay: Bool = true public var blocksBackgroundWhenInOverlay: Bool = true @@ -1221,6 +1234,35 @@ open class NavigationController: UINavigationController, ContainableController, } } + public func updatePossibleControllerDropContent(content: NavigationControllerDropContent?) { + if let rootContainer = self.rootContainer { + switch rootContainer { + case let .flat(container): + if let controller = container.controllers.last { + controller.updatePossibleControllerDropContent(content: content) + } + case .split: + break + } + } + } + + public func acceptPossibleControllerDropContent(content: NavigationControllerDropContent) -> Bool { + if let rootContainer = self.rootContainer { + switch rootContainer { + case let .flat(container): + if let controller = container.controllers.last { + if controller.acceptPossibleControllerDropContent(content: content) { + return true + } + } + case .split: + break + } + } + return false + } + override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { preconditionFailure() } diff --git a/submodules/Display/Source/NavigationBarBadge.swift b/submodules/Display/Source/NavigationBarBadge.swift index 333bcfb825..e12b716ef5 100644 --- a/submodules/Display/Source/NavigationBarBadge.swift +++ b/submodules/Display/Source/NavigationBarBadge.swift @@ -45,6 +45,7 @@ public final class NavigationBarBadgeNode: ASDisplayNode { self.textColor = textColor self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0) self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) + self.textNode.redrawIfPossible() } override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 9d40581015..2fee5bd184 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -644,6 +644,13 @@ public enum TabBarItemContextActionType { open func tabBarItemSwipeAction(direction: TabBarItemSwipeDirection) { } + + open func updatePossibleControllerDropContent(content: NavigationControllerDropContent?) { + } + + open func acceptPossibleControllerDropContent(content: NavigationControllerDropContent) -> Bool { + return false + } } func traceIsOpaque(layer: CALayer, rect: CGRect) -> Bool { diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 66e1f24dd4..503adf38e0 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1287,7 +1287,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { mediaManager?.setOverlayVideoNode(nil) }) expandImpl = { [weak overlayNode] in - guard let contentInfo = item.contentInfo else { + guard let contentInfo = item.contentInfo, let overlayNode = overlayNode else { return } @@ -1302,7 +1302,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { baseNavigationController?.view.endEditing(true) - (baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { id, media in + (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) diff --git a/submodules/SettingsUI/Sources/DebugController.swift b/submodules/SettingsUI/Sources/DebugController.swift index 90bd1e1cbf..3351a26003 100644 --- a/submodules/SettingsUI/Sources/DebugController.swift +++ b/submodules/SettingsUI/Sources/DebugController.swift @@ -42,6 +42,7 @@ private enum DebugControllerSection: Int32 { case logs case logging case experiments + case videoExperiments case info } @@ -70,6 +71,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case knockoutWallpaper(PresentationTheme, Bool) case alternativeFolderTabs(Bool) case videoCalls(Bool) + case videoCallsInfo(PresentationTheme, String) case hostInfo(PresentationTheme, String) case versionInfo(PresentationTheme) @@ -83,8 +85,10 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: return DebugControllerSection.experiments.rawValue - case .clearTips, .reimport, .resetData, .resetDatabase, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .alternativeFolderTabs, .videoCalls: + case .clearTips, .reimport, .resetData, .resetDatabase, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .alternativeFolderTabs: return DebugControllerSection.experiments.rawValue + case .videoCalls, .videoCallsInfo: + return DebugControllerSection.videoExperiments.rawValue case .hostInfo, .versionInfo: return DebugControllerSection.info.rawValue } @@ -140,10 +144,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 23 case .videoCalls: return 24 - case .hostInfo: + case .videoCallsInfo: return 25 - case .versionInfo: + case .hostInfo: return 26 + case .versionInfo: + return 27 } } @@ -542,7 +548,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { }).start() }) case let .videoCalls(value): - return ItemListSwitchItem(presentationData: presentationData, title: "Video", value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: "Experimental Feature", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings as? ExperimentalUISettings ?? ExperimentalUISettings.defaultSettings @@ -551,6 +557,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) + case let .videoCallsInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .hostInfo(theme, string): return ItemListTextItem(presentationData: presentationData, text: .plain(string), sectionId: self.section) case let .versionInfo(theme): @@ -595,6 +603,7 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS entries.append(.knockoutWallpaper(presentationData.theme, experimentalSettings.knockoutWallpaper)) entries.append(.alternativeFolderTabs(experimentalSettings.foldersTabAtBottom)) entries.append(.videoCalls(experimentalSettings.videoCalls)) + entries.append(.videoCallsInfo(presentationData.theme, "Enables experimental transmission of electromagnetic radiation synchronized with pressure waves. Needs to be enabled on both sides.")) if let backupHostOverride = networkSettings?.backupHostOverride { entries.append(.hostInfo(presentationData.theme, "Host: \(backupHostOverride)")) diff --git a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift index 075fa1a452..70e63b132c 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) { + convenience init(rootControllerTheme: PresentationTheme, hideBackground: Bool = false) { let theme = rootControllerTheme.rootController.navigationBar - self.init(buttonColor: theme.buttonColor, disabledButtonColor: theme.disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: theme.backgroundColor, separatorColor: theme.separatorColor, badgeBackgroundColor: theme.badgeBackgroundColor, badgeStrokeColor: 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: theme.badgeBackgroundColor, badgeStrokeColor: hideBackground ? .clear : theme.badgeStrokeColor, badgeTextColor: theme.badgeTextColor) } } @@ -62,6 +62,10 @@ 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(presentationTheme: PresentationTheme, presentationStrings: PresentationStrings) { self.init(theme: NavigationBarTheme(rootControllerTheme: presentationTheme), strings: NavigationBarStrings(presentationStrings: presentationStrings)) } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index c5911376f1..79713d4e9d 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -320,6 +320,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private let peekData: ChatPeekTimeout? private let peekTimerDisposable = MetaDisposable() + + private var hasEmbeddedTitleContent = false public override var customData: Any? { return self.chatLocation @@ -373,7 +375,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .inline: navigationBarPresentationData = nil default: - navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) + navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: true) } super.init(context: context, navigationBarPresentationData: navigationBarPresentationData, mediaAccessoryPanelVisibility: mediaAccessoryPanelVisibility, locationBroadcastPanelSource: locationBroadcastPanelSource) @@ -2779,8 +2781,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .inline: self.statusBar.statusBarStyle = .Ignore } - self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) - self.chatTitleView?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.updateNavigationBarPresentation() self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in var state = state state = state.updatedTheme(self.presentationData.theme) @@ -2794,6 +2795,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.currentContextController?.updateTheme(presentationData: self.presentationData) } + private func updateNavigationBarPresentation() { + let navigationBarTheme: NavigationBarTheme + + if self.hasEmbeddedTitleContent { + navigationBarTheme = NavigationBarTheme(rootControllerTheme: defaultDarkPresentationTheme, hideBackground: true) + } else { + navigationBarTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme, hideBackground: true) + } + + self.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) + + self.chatTitleView?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, hasEmbeddedTitleContent: self.hasEmbeddedTitleContent) + } + override public func loadDisplayNode() { self.displayNode = ChatControllerNode(context: self.context, chatLocation: self.chatLocation, subject: self.subject, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, controller: self) @@ -4735,6 +4750,32 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } + self.chatDisplayNode.updateHasEmbeddedTitleContent = { [weak self] hasEmbeddedTitleContent in + guard let strongSelf = self else { + return + } + if strongSelf.hasEmbeddedTitleContent != hasEmbeddedTitleContent { + strongSelf.hasEmbeddedTitleContent = hasEmbeddedTitleContent + + if strongSelf.hasEmbeddedTitleContent { + strongSelf.statusBar.statusBarStyle = .White + } else { + strongSelf.statusBar.statusBarStyle = strongSelf.presentationData.theme.rootController.statusBarStyle.style + } + + if let navigationBar = strongSelf.navigationBar { + if let navigationBarCopy = navigationBar.view.snapshotContentTree() { + navigationBar.view.superview?.insertSubview(navigationBarCopy, aboveSubview: navigationBar.view) + navigationBarCopy.alpha = 0.0 + navigationBarCopy.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak navigationBarCopy] _ in + navigationBarCopy?.removeFromSuperview() + }) + } + } + strongSelf.updateNavigationBarPresentation() + } + } + self.interfaceInteraction = interfaceInteraction if let search = self.focusOnSearchAfterAppearance { @@ -5143,7 +5184,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G switch self.presentationInterfaceState.mode { case .standard, .inline: - break + break case .overlay: if case .Ignore = self.statusBar.statusBarStyle { } else if layout.safeInsets.top.isZero { @@ -5503,7 +5544,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G switch updatedChatPresentationInterfaceState.mode { case .standard: - self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + if self.hasEmbeddedTitleContent { + self.statusBar.statusBarStyle = .White + } else { + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + } self.deferScreenEdgeGestures = [] case .overlay: self.deferScreenEdgeGestures = [.top] @@ -9256,6 +9301,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.focusOnSearchAfterAppearance = (domain, query) self.interfaceInteraction?.beginMessageSearch(domain, query) } + + override public func updatePossibleControllerDropContent(content: NavigationControllerDropContent?) { + self.chatDisplayNode.updateEmbeddedTitlePeekContent(content: content) + } + + override public func acceptPossibleControllerDropContent(content: NavigationControllerDropContent) -> Bool { + return self.chatDisplayNode.acceptEmbeddedTitlePeekContent(content: content) + } } private final class ContextControllerContentSourceImpl: ContextControllerContentSource { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 46f74fa9db..b4abb27535 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -12,6 +12,15 @@ import TextFormat import AccountContext import TelegramNotices import ReactionSelectionNode +import TelegramUniversalVideoContent + +final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { + let itemNode: OverlayMediaItemNode + + init(itemNode: OverlayMediaItemNode) { + self.itemNode = itemNode + } +} private final class ChatControllerNodeView: UITracingLayerView, WindowInputAccessoryHeightProvider, PreviewingHostView { var inputAccessoryHeight: (() -> CGFloat)? @@ -56,6 +65,175 @@ private struct ChatControllerNodeDerivedLayoutState { var upperInputPositionBound: CGFloat? } +private final class ChatEmbeddedTitleContentNode: ASDisplayNode { + private let context: AccountContext + private let backgroundNode: ASDisplayNode + private let videoNode: OverlayUniversalVideoNode + + private var validLayout: (CGSize, CGFloat, CGFloat)? + + private let dismissed: () -> Void + private let interactiveExtensionUpdated: (ContainedViewLayoutTransition) -> Void + + private(set) var interactiveExtension: CGFloat = 0.0 + private var freezeInteractiveExtension = false + + init(context: AccountContext, videoNode: OverlayUniversalVideoNode, interactiveExtensionUpdated: @escaping (ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void) { + self.dismissed = dismissed + self.interactiveExtensionUpdated = interactiveExtensionUpdated + + self.context = context + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.backgroundColor = .black + + self.videoNode = videoNode + + super.init() + + self.clipsToBounds = true + + self.addSubnode(self.backgroundNode) + + self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + break + case .changed: + let translation = recognizer.translation(in: self.view) + + func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat { + let bandedOffset = offset - bandingStart + let range: CGFloat = 600.0 + let coefficient: CGFloat = 0.4 + return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range + } + + let offset = rubberBandingOffset(offset: translation.y, bandingStart: 0.0) + + if translation.y > 80.0 { + self.freezeInteractiveExtension = true + self.videoNode.customExpand?() + } else { + self.interactiveExtension = max(0.0, offset) + self.interactiveExtensionUpdated(.immediate) + } + case .cancelled, .ended: + if !freezeInteractiveExtension { + self.interactiveExtension = 0.0 + self.interactiveExtensionUpdated(.animated(duration: 0.3, curve: .spring)) + } + default: + break + } + } + + func calculateHeight(width: CGFloat) -> CGFloat { + return self.videoNode.content.dimensions.aspectFilled(CGSize(width: width, height: 16.0)).height + } + + func updateLayout(size: CGSize, topInset: CGFloat, interactiveExtension: CGFloat, transition: ContainedViewLayoutTransition, transitionSurface: ASDisplayNode?, navigationBar: NavigationBar?) { + let isFirstTime = self.validLayout == nil + + self.validLayout = (size, topInset, interactiveExtension) + + let videoFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset + interactiveExtension), size: CGSize(width: size.width, height: size.height - topInset - interactiveExtension)) + + if isFirstTime, let transitionSurface = transitionSurface { + 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) + + 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) + } + + self.videoNode.updateRoundCorners(false, transition: .animated(duration: 0.25, curve: .spring)) + self.videoNode.showControls() + + 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) + + transition.updateFrame(node: strongSelf.videoNode, frame: targetFrame) + strongSelf.videoNode.updateLayout(targetFrame.size, transition: transition) + } + + strongSelf.dismissed() + } + + self.videoNode.customClose = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.videoNode.customClose = nil + strongSelf.dismissed() + } + } + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + + if self.videoNode.supernode == self { + self.videoNode.layer.transform = CATransform3DIdentity + transition.updateFrame(node: self.videoNode, frame: videoFrame) + } + } +} + +enum ChatEmbeddedTitlePeekContent: Equatable { + case none + case peek +} + class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let context: AccountContext let chatLocation: ChatLocation @@ -63,6 +241,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private weak var controller: ChatControllerImpl? let navigationBar: NavigationBar? + private let navigationBarBackroundNode: ASDisplayNode + private let navigationBarSeparatorNode: ASDisplayNode private var backgroundEffectNode: ASDisplayNode? private var containerBackgroundNode: ASImageNode? @@ -204,6 +384,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var onLayoutCompletions: [(ContainedViewLayoutTransition) -> Void] = [] + private var embeddedTitlePeekContent: ChatEmbeddedTitlePeekContent = .none + private var embeddedTitleContentNode: ChatEmbeddedTitleContentNode? + private var dismissedEmbeddedTitleContentNode: ChatEmbeddedTitleContentNode? + init(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: MediaAutoDownloadSettings, navigationBar: NavigationBar?, controller: ChatControllerImpl?) { self.context = context self.chatLocation = chatLocation @@ -248,6 +432,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme) self.navigateButtons.accessibilityElementsHidden = true + self.navigationBarBackroundNode = ASDisplayNode() + self.navigationBarBackroundNode.backgroundColor = chatPresentationInterfaceState.theme.rootController.navigationBar.backgroundColor + + self.navigationBarSeparatorNode = ASDisplayNode() + self.navigationBarSeparatorNode.backgroundColor = chatPresentationInterfaceState.theme.rootController.navigationBar.separatorColor + super.init() self.controller?.presentationContext.topLevelSubview = { [weak self] in @@ -329,6 +519,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.addSubnode(self.navigateButtons) + self.addSubnode(self.navigationBarBackroundNode) + self.addSubnode(self.navigationBarSeparatorNode) + self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.textInputPanelNode = ChatTextInputPanelNode(presentationInterfaceState: chatPresentationInterfaceState, presentController: { [weak self] controller in @@ -696,12 +889,63 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } else { insets = layout.insets(options: [.input]) } - if case .overlay = self.chatPresentationInterfaceState.mode { - insets.top = 44.0 + + let statusBarHeight = layout.insets(options: [.statusBar]).top + + 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 { + self.insertSubnode(embeddedTitleContentNode, aboveSubnode: self.navigationBarBackroundNode) + + var previousTopInset = insets.top + if case .overlay = self.chatPresentationInterfaceState.mode { + previousTopInset = 44.0 + } else { + previousTopInset += navigationBarHeight + } + + if case .peek = self.embeddedTitlePeekContent { + previousTopInset += 32.0 + } + + embeddedTitleContentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: previousTopInset)) + transition.updateFrame(node: embeddedTitleContentNode, frame: CGRect(origin: CGPoint(), size: embeddedSize)) + embeddedTitleContentNode.updateLayout(size: embeddedSize, topInset: statusBarHeight, interactiveExtension: embeddedTitleContentNode.interactiveExtension, transition: .immediate, transitionSurface: self, navigationBar: self.navigationBar) + } else { + transition.updateFrame(node: embeddedTitleContentNode, frame: CGRect(origin: CGPoint(), size: embeddedSize)) + embeddedTitleContentNode.updateLayout(size: embeddedSize, topInset: statusBarHeight, interactiveExtension: embeddedTitleContentNode.interactiveExtension, transition: transition, transitionSurface: self, navigationBar: self.navigationBar) + } + + insets.top += embeddedSize.height } else { - insets.top += navigationBarHeight + if case .overlay = self.chatPresentationInterfaceState.mode { + insets.top = 44.0 + } else { + insets.top += navigationBarHeight + } + + if case .peek = self.embeddedTitlePeekContent { + insets.top += 32.0 + } } + if let dismissedEmbeddedTitleContentNode = self.dismissedEmbeddedTitleContentNode { + self.dismissedEmbeddedTitleContentNode = nil + if transition.isAnimated { + dismissedEmbeddedTitleContentNode.alpha = 0.0 + dismissedEmbeddedTitleContentNode.layer.allowsGroupOpacity = true + dismissedEmbeddedTitleContentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { [weak dismissedEmbeddedTitleContentNode] _ in + dismissedEmbeddedTitleContentNode?.removeFromSupernode() + }) + transition.updateFrame(node: dismissedEmbeddedTitleContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: insets.top))) + } else { + dismissedEmbeddedTitleContentNode.removeFromSupernode() + } + } + + transition.updateFrame(node: self.navigationBarBackroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: insets.top))) + transition.updateFrame(node: self.navigationBarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + var wrappingInsets = UIEdgeInsets() if case .overlay = self.chatPresentationInterfaceState.mode { let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 8.0 + layout.safeInsets.left) @@ -1518,6 +1762,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } self.updatePlainInputSeparator(transition: .immediate) self.inputPanelBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelSeparatorColor + + self.navigationBarBackroundNode.backgroundColor = chatPresentationInterfaceState.theme.rootController.navigationBar.backgroundColor + self.navigationBarSeparatorNode.backgroundColor = chatPresentationInterfaceState.theme.rootController.navigationBar.separatorColor } let keepSendButtonEnabled = chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil || chatPresentationInterfaceState.interfaceState.editMessage != nil @@ -2384,4 +2631,58 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { func animateQuizCorrectOptionSelected() { self.view.insertSubview(ConfettiView(frame: self.view.bounds), aboveSubview: self.historyNode.view) } + + func updateEmbeddedTitlePeekContent(content: NavigationControllerDropContent?) { + guard let (_, navigationHeight) = self.validLayout else { + return + } + var peekContent: ChatEmbeddedTitlePeekContent = .none + if let content = content, let item = content.item as? VideoNavigationControllerDropContentItem, let _ = item.itemNode as? OverlayUniversalVideoNode { + if content.position.y < navigationHeight + 32.0 { + peekContent = .peek + } + } + if self.embeddedTitlePeekContent != peekContent { + self.embeddedTitlePeekContent = peekContent + self.requestLayout(.animated(duration: 0.3, curve: .spring)) + } + } + + var updateHasEmbeddedTitleContent: ((Bool) -> Void)? + + func acceptEmbeddedTitlePeekContent(content: NavigationControllerDropContent) -> Bool { + guard let (_, navigationHeight) = self.validLayout else { + return false + } + if content.position.y >= navigationHeight + 32.0 { + 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 + 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?(false) + } + }) + self.embeddedTitleContentNode = embeddedTitleContentNode + self.embeddedTitlePeekContent = .none + self.updateHasEmbeddedTitleContent?(true) + DispatchQueue.main.async { + self.requestLayout(.animated(duration: 0.25, curve: .spring)) + } + + return true + } + return false + } } diff --git a/submodules/TelegramUI/Sources/ChatTitleView.swift b/submodules/TelegramUI/Sources/ChatTitleView.swift index 924258fb13..8228544ff7 100644 --- a/submodules/TelegramUI/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Sources/ChatTitleView.swift @@ -23,61 +23,6 @@ enum ChatTitleContent { case custom(String) } -private final class ChatTitleNetworkStatusNode: ASDisplayNode { - private var theme: PresentationTheme - - private let titleNode: ImmediateTextNode - private let activityIndicator: ActivityIndicator - - var title: String = "" { - didSet { - if self.title != oldValue { - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) - } - } - } - - init(theme: PresentationTheme) { - self.theme = theme - - self.titleNode = ImmediateTextNode() - self.titleNode.isUserInteractionEnabled = false - self.titleNode.displaysAsynchronously = false - self.titleNode.maximumNumberOfLines = 1 - self.titleNode.isOpaque = false - self.titleNode.isUserInteractionEnabled = false - - self.activityIndicator = ActivityIndicator(type: .custom(theme.rootController.navigationBar.primaryTextColor, 22.0, 1.5, false), speed: .slow) - let activityIndicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) - self.activityIndicator.frame = CGRect(origin: CGPoint(), size: activityIndicatorSize) - - super.init() - - self.addSubnode(self.titleNode) - self.addSubnode(self.activityIndicator) - } - - func updateTheme(theme: PresentationTheme) { - self.theme = theme - - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.medium(24.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) - self.activityIndicator.type = .custom(self.theme.rootController.navigationBar.primaryTextColor, 22.0, 1.5, false) - } - - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - let indicatorSize = self.activityIndicator.bounds.size - let indicatorPadding = indicatorSize.width + 6.0 - - let titleSize = self.titleNode.updateLayout(CGSize(width: max(1.0, size.width - indicatorPadding), height: size.height)) - let combinedHeight = titleSize.height - - let titleFrame = CGRect(origin: CGPoint(x: indicatorPadding + floor((size.width - titleSize.width - indicatorPadding) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) - transition.updateFrame(node: self.titleNode, frame: titleFrame) - - transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: titleFrame.minX - indicatorSize.width - 4.0, y: titleFrame.minY - 1.0), size: indicatorSize)) - } -} - private enum ChatTitleIcon { case none case lock @@ -88,6 +33,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { private let account: Account private var theme: PresentationTheme + private var hasEmbeddedTitleContent: Bool = false private var strings: PresentationStrings private var dateTimeFormat: PresentationDateTimeFormat private var nameDisplayOrder: PresentationPersonNameOrder @@ -120,43 +66,6 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } private func updateNetworkStatusNode(networkState: AccountNetworkState, layout: ContainerViewLayout?) { - var isOnline = false - if case .online = networkState { - isOnline = true - } - - /*if isOnline || layout?.metrics.widthClass == .regular { - self.contentContainer.isHidden = false - if let networkStatusNode = self.networkStatusNode { - self.networkStatusNode = nil - networkStatusNode.removeFromSupernode() - } - } else { - self.contentContainer.isHidden = true - let statusNode: ChatTitleNetworkStatusNode - if let current = self.networkStatusNode { - statusNode = current - } else { - statusNode = ChatTitleNetworkStatusNode(theme: self.theme) - self.networkStatusNode = statusNode - self.insertSubview(statusNode.view, aboveSubview: self.contentContainer.view) - } - switch self.networkState { - case .waitingForNetwork: - statusNode.title = self.strings.State_WaitingForNetwork - case let .connecting(proxy): - if let layout = layout, proxy != nil && layout.size.width > 320.0 { - statusNode.title = self.strings.State_ConnectingToProxy - } else { - statusNode.title = self.strings.State_Connecting - } - case .updating: - statusNode.title = self.strings.State_Updating - case .online: - break - } - }*/ - self.setNeedsLayout() } @@ -183,6 +92,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { var titleContent: ChatTitleContent? { didSet { if let titleContent = self.titleContent { + let titleTheme = self.hasEmbeddedTitleContent ? defaultDarkPresentationTheme : self.theme + var string: NSAttributedString? var titleLeftIcon: ChatTitleIcon = .none var titleRightIcon: ChatTitleIcon = .none @@ -192,20 +103,20 @@ final class ChatTitleView: UIView, NavigationBarTitleView { case let .peer(peerView, _, isScheduledMessages): if isScheduledMessages { if peerView.peerId == self.account.peerId { - string = NSAttributedString(string: self.strings.ScheduledMessages_RemindersTitle, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + string = NSAttributedString(string: self.strings.ScheduledMessages_RemindersTitle, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor) } else { - string = NSAttributedString(string: self.strings.ScheduledMessages_Title, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + string = NSAttributedString(string: self.strings.ScheduledMessages_Title, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor) } isEnabled = false } else { if let peer = peerViewMainPeer(peerView) { if peerView.peerId == self.account.peerId { - string = NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + string = NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor) } else { if !peerView.peerIsContact, let user = peer as? TelegramUser, !user.flags.contains(.isSupport), user.botInfo == nil, let phone = user.phone, !phone.isEmpty { - string = NSAttributedString(string: formatPhoneNumber(phone), font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + string = NSAttributedString(string: formatPhoneNumber(phone), font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor) } else { - string = NSAttributedString(string: peer.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + string = NSAttributedString(string: peer.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor) } } titleScamIcon = peer.isScam @@ -220,9 +131,9 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } } case .group: - string = NSAttributedString(string: "Feed", font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + string = NSAttributedString(string: "Feed", font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor) case let .custom(text): - string = NSAttributedString(string: text, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + string = NSAttributedString(string: text, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor) } if let string = string, self.titleNode.attributedText == nil || !self.titleNode.attributedText!.isEqual(to: string) { @@ -234,7 +145,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.titleLeftIcon = titleLeftIcon switch titleLeftIcon { case .lock: - self.titleLeftIconNode.image = PresentationResourcesChat.chatTitleLockIcon(self.theme) + self.titleLeftIconNode.image = PresentationResourcesChat.chatTitleLockIcon(titleTheme) default: self.titleLeftIconNode.image = nil } @@ -243,7 +154,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if titleScamIcon != self.titleScamIcon { self.titleScamIcon = titleScamIcon - self.titleCredibilityIconNode.image = titleScamIcon ? PresentationResourcesChatList.scamIcon(self.theme, type: .regular) : nil + self.titleCredibilityIconNode.image = titleScamIcon ? PresentationResourcesChatList.scamIcon(titleTheme, type: .regular) : nil self.setNeedsLayout() } @@ -251,7 +162,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.titleRightIcon = titleRightIcon switch titleRightIcon { case .mute: - self.titleRightIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(self.theme) + self.titleRightIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(titleTheme) default: self.titleRightIconNode.image = nil } @@ -278,6 +189,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } } + let titleTheme = self.hasEmbeddedTitleContent ? defaultDarkPresentationTheme : self.theme + var state = ChatTitleActivityNodeState.none switch self.networkState { case .waitingForNetwork, .connecting, .updating: @@ -285,14 +198,14 @@ final class ChatTitleView: UIView, NavigationBarTitleView { switch self.networkState { case .waitingForNetwork: infoText = self.strings.ChatState_WaitingForNetwork - case let .connecting(proxy): + case .connecting: infoText = self.strings.ChatState_Connecting case .updating: infoText = self.strings.ChatState_Updating case .online: infoText = "" } - state = .info(NSAttributedString(string: infoText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor), .generic) + state = .info(NSAttributedString(string: infoText, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor), .generic) case .online: if let (peerId, inputActivities) = self.inputActivities, !inputActivities.isEmpty, inputActivitiesAllowed { var stringValue = "" @@ -336,7 +249,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } } } - let color = self.theme.rootController.navigationBar.accentTextColor + let color = titleTheme.rootController.navigationBar.accentTextColor let string = NSAttributedString(string: stringValue, font: Font.regular(13.0), textColor: color) switch mergedActivity { case .typingText: @@ -357,21 +270,21 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if let peer = peerViewMainPeer(peerView) { let servicePeer = isServicePeer(peer) if peer.id == self.account.peerId || isScheduledMessages { - let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } else if let user = peer as? TelegramUser { if servicePeer { - let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } else if user.flags.contains(.isSupport) { let statusText = self.strings.Bot_GenericSupportStatus - let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } else if let _ = user.botInfo { let statusText = self.strings.Bot_GenericBotStatus - let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } else if let peer = peerViewMainPeer(peerView) { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 @@ -383,10 +296,10 @@ final class ChatTitleView: UIView, NavigationBarTitleView { userPresence = TelegramUserPresence(status: .none, lastActivity: 0) } let (string, activity) = stringAndActivityForUserPresence(strings: self.strings, dateTimeFormat: self.dateTimeFormat, presence: userPresence, relativeTo: Int32(timestamp)) - let attributedString = NSAttributedString(string: string, font: Font.regular(13.0), textColor: activity ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.secondaryTextColor) + let attributedString = NSAttributedString(string: string, font: Font.regular(13.0), textColor: activity ? titleTheme.rootController.navigationBar.accentTextColor : titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(attributedString, activity ? .online : .lastSeenTime) } else { - let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } } else if let group = peer as? TelegramGroup { @@ -408,11 +321,11 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if onlineCount > 1 { let string = NSMutableAttributedString() - string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(group.participantCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) - string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) + string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(group.participantCount))), ", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)) + string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineCount)), font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)) state = .info(string, .generic) } else { - let string = NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } } else if let channel = peer as? TelegramChannel { @@ -420,17 +333,17 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if memberCount == 0 { let string: NSAttributedString if case .group = channel.info { - string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) } else { - string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) } state = .info(string, .generic) } else { if case .group = channel.info, let onlineMemberCount = onlineMemberCount, onlineMemberCount > 1 { let string = NSMutableAttributedString() - string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(memberCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) - string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineMemberCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) + string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(memberCount))), ", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)) + string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineMemberCount)), font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)) state = .info(string, .generic) } else { let membersString: String @@ -439,17 +352,17 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } else { membersString = strings.Conversation_StatusSubscribers(memberCount) } - let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } } } else { switch channel.info { case .group: - let string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) case .broadcast: - let string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } } @@ -551,11 +464,11 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } } - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings, hasEmbeddedTitleContent: Bool) { self.theme = theme + self.hasEmbeddedTitleContent = hasEmbeddedTitleContent self.strings = strings - //self.networkStatusNode?.updateTheme(theme: theme) let titleContent = self.titleContent self.titleContent = titleContent self.updateStatus() @@ -568,8 +481,6 @@ final class ChatTitleView: UIView, NavigationBarTitleView { func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) { self.validLayout = (size, clearBounds) - let transition: ContainedViewLayoutTransition = .immediate - self.button.frame = clearBounds self.contentContainer.frame = clearBounds diff --git a/submodules/TelegramUI/Sources/OverlayMediaController.swift b/submodules/TelegramUI/Sources/OverlayMediaController.swift index 08a36a3450..7c7e93baef 100644 --- a/submodules/TelegramUI/Sources/OverlayMediaController.swift +++ b/submodules/TelegramUI/Sources/OverlayMediaController.swift @@ -11,6 +11,9 @@ public final class OverlayMediaControllerImpl: ViewController, OverlayMediaContr return self.displayNode as! OverlayMediaControllerNode } + public var updatePossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem?) -> Void)? + public var embedPossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem) -> Bool)? + public init() { super.init(navigationBarPresentationData: nil) @@ -22,7 +25,11 @@ public final class OverlayMediaControllerImpl: ViewController, OverlayMediaContr } override public func loadDisplayNode() { - self.displayNode = OverlayMediaControllerNode() + self.displayNode = OverlayMediaControllerNode(updatePossibleEmbeddingItem: { [weak self] item in + self?.updatePossibleEmbeddingItem?(item) + }, embedPossibleEmbeddingItem: { [weak self] item in + return self?.embedPossibleEmbeddingItem?(item) ?? false + }) self.displayNodeDidLoad() } diff --git a/submodules/TelegramUI/Sources/OverlayMediaControllerNode.swift b/submodules/TelegramUI/Sources/OverlayMediaControllerNode.swift index 511fa97937..52cc221d47 100644 --- a/submodules/TelegramUI/Sources/OverlayMediaControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayMediaControllerNode.swift @@ -28,7 +28,12 @@ private final class OverlayMediaVideoNodeData { } } + + final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { + private let updatePossibleEmbeddingItem: (OverlayMediaControllerEmbeddingItem?) -> Void + private let embedPossibleEmbeddingItem: (OverlayMediaControllerEmbeddingItem) -> Bool + private var videoNodes: [OverlayMediaVideoNodeData] = [] private var validLayout: ContainerViewLayout? @@ -40,7 +45,10 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega private var pinchingNode: OverlayMediaItemNode? private var pinchingNodeInitialSize: CGSize? - override init() { + init(updatePossibleEmbeddingItem: @escaping (OverlayMediaControllerEmbeddingItem?) -> Void, embedPossibleEmbeddingItem: @escaping (OverlayMediaControllerEmbeddingItem) -> Bool) { + self.updatePossibleEmbeddingItem = updatePossibleEmbeddingItem + self.embedPossibleEmbeddingItem = embedPossibleEmbeddingItem + super.init() self.setViewBlock({ @@ -329,34 +337,46 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega draggingNode.updateMinimizedEdge(nil, adjusting: true) } draggingNode.frame = nodeFrame + self.updatePossibleEmbeddingItem(OverlayMediaControllerEmbeddingItem( + position: nodeFrame.center, + itemNode: draggingNode + )) } case .ended, .cancelled: if let draggingNode = self.draggingNode, let validLayout = self.validLayout, let index = self.videoNodes.firstIndex(where: { $0.node === draggingNode }){ let nodeSize = self.videoNodes[index].currentSize let previousFrame = draggingNode.frame - let (updatedLocation, shouldDismiss) = self.nodeLocationForPosition(layout: validLayout, position: CGPoint(x: previousFrame.midX, y: previousFrame.midY), velocity: recognizer.velocity(in: self.view), size: nodeSize, tempExtendedTopInset: draggingNode.tempExtendedTopInset) - - if shouldDismiss && draggingNode.isMinimizeable { - draggingNode.updateMinimizedEdge(updatedLocation.x.isZero ? .left : .right, adjusting: false) - self.videoNodes[index].isMinimized = true + if self.embedPossibleEmbeddingItem(OverlayMediaControllerEmbeddingItem( + position: previousFrame.center, + itemNode: draggingNode + )) { + self.draggingNode = nil } else { - draggingNode.updateMinimizedEdge(nil, adjusting: true) - self.videoNodes[index].isMinimized = false - } - - if let group = draggingNode.group { - self.locationByGroup[group] = updatedLocation - } - self.videoNodes[index].location = updatedLocation - - draggingNode.frame = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: updatedLocation, hidden: !draggingNode.hasAttachedContext, isMinimized: self.videoNodes[index].isMinimized, tempExtendedTopInset: draggingNode.tempExtendedTopInset), size: nodeSize) - draggingNode.layer.animateFrame(from: previousFrame, to: draggingNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.draggingNode = nil - - if shouldDismiss && !draggingNode.isMinimizeable { - draggingNode.dismiss() + let (updatedLocation, shouldDismiss) = self.nodeLocationForPosition(layout: validLayout, position: CGPoint(x: previousFrame.midX, y: previousFrame.midY), velocity: recognizer.velocity(in: self.view), size: nodeSize, tempExtendedTopInset: draggingNode.tempExtendedTopInset) + + if shouldDismiss && draggingNode.isMinimizeable { + draggingNode.updateMinimizedEdge(updatedLocation.x.isZero ? .left : .right, adjusting: false) + self.videoNodes[index].isMinimized = true + } else { + draggingNode.updateMinimizedEdge(nil, adjusting: true) + self.videoNodes[index].isMinimized = false + } + + if let group = draggingNode.group { + self.locationByGroup[group] = updatedLocation + } + self.videoNodes[index].location = updatedLocation + + draggingNode.frame = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: updatedLocation, hidden: !draggingNode.hasAttachedContext, isMinimized: self.videoNodes[index].isMinimized, tempExtendedTopInset: draggingNode.tempExtendedTopInset), size: nodeSize) + draggingNode.layer.animateFrame(from: previousFrame, to: draggingNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.draggingNode = nil + + if shouldDismiss && !draggingNode.isMinimizeable { + draggingNode.dismiss() + } } + self.updatePossibleEmbeddingItem(nil) } default: break diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 72a0a485cd..3883d1e080 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -2281,14 +2281,14 @@ final class PeerInfoHeaderNode: ASDisplayNode { if buttonKeys.count > 3 { if self.isOpenedFromChat { switch buttonKey { - case .message, .search: + case .message, .search, .videoCall: hiddenWhileExpanded = true default: hiddenWhileExpanded = false } } else { switch buttonKey { - case .mute, .search: + case .mute, .search, .videoCall: hiddenWhileExpanded = true default: hiddenWhileExpanded = false diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 2229784407..f7108af284 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -207,6 +207,43 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.mediaManager = MediaManagerImpl(accountManager: accountManager, inForeground: applicationBindings.applicationInForeground, presentationData: presentationData) + self.mediaManager.overlayMediaManager.updatePossibleEmbeddingItem = { [weak self] item in + guard let strongSelf = self else { + return + } + guard let navigationController = strongSelf.mainWindow?.viewController as? NavigationController else { + return + } + var content: NavigationControllerDropContent? + if let item = item { + content = NavigationControllerDropContent( + position: item.position, + item: VideoNavigationControllerDropContentItem( + itemNode: item.itemNode + ) + ) + } + + navigationController.updatePossibleControllerDropContent(content: content) + } + + self.mediaManager.overlayMediaManager.embedPossibleEmbeddingItem = { [weak self] item in + guard let strongSelf = self else { + return false + } + guard let navigationController = strongSelf.mainWindow?.viewController as? NavigationController else { + return false + } + let content = NavigationControllerDropContent( + position: item.position, + item: VideoNavigationControllerDropContentItem( + itemNode: item.itemNode + ) + ) + + return navigationController.acceptPossibleControllerDropContent(content: content) + } + self._autodownloadSettings.set(.single(initialPresentationDataAndSettings.autodownloadSettings) |> then(accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings]) |> map { sharedData in diff --git a/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift b/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift index 38a13e0eac..6265ef0b31 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift @@ -10,7 +10,7 @@ import TelegramAudio import AccountContext public final class OverlayUniversalVideoNode: OverlayMediaItemNode { - private let content: UniversalVideoContent + public let content: UniversalVideoContent private let videoNode: UniversalVideoNode private let decoration: OverlayVideoDecoration @@ -30,8 +30,16 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode { } } + private let defaultExpand: () -> Void + public var customExpand: (() -> Void)? + public var customClose: (() -> 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 unminimizeImpl: (() -> Void)? var togglePlayPauseImpl: (() -> Void)? var closeImpl: (() -> Void)? @@ -40,7 +48,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode { }, togglePlayPause: { togglePlayPauseImpl?() }, expand: { - expand() + expandImpl?() }, close: { closeImpl?() }) @@ -49,6 +57,17 @@ 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() + } + } + unminimizeImpl = { [weak self] in self?.unminimize?() } @@ -57,6 +76,10 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode { } closeImpl = { [weak self] in if let strongSelf = self { + if let customClose = strongSelf.customClose { + customClose() + return + } if strongSelf.videoNode.hasAttachedContext { strongSelf.videoNode.continuePlayingWithoutSound() } @@ -104,18 +127,32 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode { override public func updateLayout(_ size: CGSize) { if size != self.validLayoutSize { - self.updateLayoutImpl(size) + self.updateLayoutImpl(size, transition: .immediate) } } - private func updateLayoutImpl(_ size: CGSize) { + public func updateLayout(_ size: CGSize, transition: ContainedViewLayoutTransition) { + if size != self.validLayoutSize { + self.updateLayoutImpl(size, transition: transition) + } + } + + private func updateLayoutImpl(_ size: CGSize, transition: ContainedViewLayoutTransition) { self.validLayoutSize = size - self.videoNode.frame = CGRect(origin: CGPoint(), size: size) - self.videoNode.updateLayout(size: size, transition: .immediate) + transition.updateFrame(node: self.videoNode, frame: CGRect(origin: CGPoint(), size: size)) + self.videoNode.updateLayout(size: size, transition: transition) } override public func updateMinimizedEdge(_ edge: OverlayMediaItemMinimizationEdge?, adjusting: Bool) { self.decoration.updateMinimizedEdge(edge, adjusting: adjusting) } + + public func updateRoundCorners(_ value: Bool, transition: ContainedViewLayoutTransition) { + transition.updateCornerRadius(node: self, cornerRadius: value ? 4.0 : 0.0) + } + + public func showControls() { + self.decoration.showControls() + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift b/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift index 4ec927ab21..eb6ed63083 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/OverlayVideoDecoration.swift @@ -148,6 +148,13 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { } } + func showControls() { + if self.controlsNode.alpha.isZero { + self.controlsNode.alpha = 1.0 + self.controlsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + } + func setStatus(_ status: Signal) { self.controlsNode.status = status |> map { value -> MediaPlayerStatus in if let value = value { diff --git a/submodules/TelegramUniversalVideoContent/Sources/PictureInPictureVideoControlsNode.swift b/submodules/TelegramUniversalVideoContent/Sources/PictureInPictureVideoControlsNode.swift index 96677f2472..bf291fa07e 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/PictureInPictureVideoControlsNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/PictureInPictureVideoControlsNode.swift @@ -109,12 +109,12 @@ final class PictureInPictureVideoControlsNode: ASDisplayNode { let buttonSize = TGEmbedPIPButtonSize - self.leaveButton.frame = CGRect(origin: CGPoint(x: forth - floor(buttonSize.width / 2.0) - 10.0, y: size.height - buttonSize.height - 15.0), size: buttonSize) + transition.updateFrame(view: self.leaveButton, frame: CGRect(origin: CGPoint(x: forth - floor(buttonSize.width / 2.0) - 10.0, y: size.height - buttonSize.height - 15.0), size: buttonSize)) - self.pauseButton.frame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: size.height - buttonSize.height - 15.0), size: buttonSize) - self.playButton.frame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: size.height - buttonSize.height - 15.0), size: buttonSize) + transition.updateFrame(view: self.pauseButton, frame: CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: size.height - buttonSize.height - 15.0), size: buttonSize)) + transition.updateFrame(view: self.playButton, frame: CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: size.height - buttonSize.height - 15.0), size: buttonSize)) - self.closeButton.frame = CGRect(origin: CGPoint(x: self.playButton.frame.origin.x + forth + 10.0, y: size.height - buttonSize.height - 15.0), size: buttonSize) + transition.updateFrame(view: self.closeButton, frame: CGRect(origin: CGPoint(x: self.playButton.frame.origin.x + forth + 10.0, y: size.height - buttonSize.height - 15.0), size: buttonSize)) } @objc func leavePressed() {