diff --git a/submodules/ContextUI/Sources/ContextActionNode.swift b/submodules/ContextUI/Sources/ContextActionNode.swift index 03bcaea17c..cafd458e1c 100644 --- a/submodules/ContextUI/Sources/ContextActionNode.swift +++ b/submodules/ContextUI/Sources/ContextActionNode.swift @@ -17,7 +17,7 @@ public protocol ContextActionNodeProtocol: ASDisplayNode { final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol { private let action: ContextMenuActionItem - private let getController: () -> ContextController? + private let getController: () -> ContextControllerProtocol? private let actionSelected: (ContextMenuActionResult) -> Void private let backgroundNode: ASDisplayNode @@ -33,7 +33,7 @@ final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol { private var pointerInteraction: PointerInteraction? - init(presentationData: PresentationData, action: ContextMenuActionItem, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + init(presentationData: PresentationData, action: ContextMenuActionItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { self.action = action self.getController = getController self.actionSelected = actionSelected diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index 82bb62ee10..2325b2bd92 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -69,7 +69,7 @@ private final class InnerActionsContainerNode: ASDisplayNode { } } - init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, blurBackground: Bool) { + init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, blurBackground: Bool) { self.presentationData = presentationData self.feedbackTap = feedbackTap self.blurBackground = blurBackground @@ -460,7 +460,7 @@ final class ContextActionsContainerNode: ASDisplayNode { return self.additionalActionsNode != nil } - init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, displayTextSelectionTip: Bool, blurBackground: Bool) { + init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, displayTextSelectionTip: Bool, blurBackground: Bool) { self.blurBackground = blurBackground self.shadowNode = ASImageNode() self.shadowNode.displaysAsynchronously = false diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 1845ca9656..52411e7b12 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -11,6 +11,13 @@ import SwiftSignalKit private let animationDurationFactor: Double = 1.0 +public protocol ContextControllerProtocol { + var useComplexItemsTransitionAnimation: Bool { get set } + + func setItems(_ items: Signal<[ContextMenuItem], NoError>) + func dismiss(completion: (() -> Void)?) +} + public enum ContextMenuActionItemTextLayout { case singleLine case twoLinesMax @@ -66,9 +73,9 @@ public final class ContextMenuActionItem { public let badge: ContextMenuActionBadge? public let icon: (PresentationTheme) -> UIImage? public let iconSource: ContextMenuActionItemIconSource? - public let action: (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void + public let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void - public init(text: String, textColor: ContextMenuActionItemTextColor = .primary, textLayout: ContextMenuActionItemTextLayout = .twoLinesMax, textFont: ContextMenuActionItemFont = .regular, badge: ContextMenuActionBadge? = nil, icon: @escaping (PresentationTheme) -> UIImage?, iconSource: ContextMenuActionItemIconSource? = nil, action: @escaping (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void) { + public init(text: String, textColor: ContextMenuActionItemTextColor = .primary, textLayout: ContextMenuActionItemTextLayout = .twoLinesMax, textFont: ContextMenuActionItemFont = .regular, badge: ContextMenuActionBadge? = nil, icon: @escaping (PresentationTheme) -> UIImage?, iconSource: ContextMenuActionItemIconSource? = nil, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) { self.text = text self.textColor = textColor self.textFont = textFont @@ -86,7 +93,7 @@ public protocol ContextMenuCustomNode: ASDisplayNode { } public protocol ContextMenuCustomItem { - func node(presentationData: PresentationData, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode + func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode } public enum ContextMenuItem { @@ -113,7 +120,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi private let reactionSelected: (ReactionContextItem.Reaction) -> Void private let beganAnimatingOut: () -> Void private let attemptTransitionControllerIntoNavigation: () -> Void - private let getController: () -> ContextController? + private let getController: () -> ContextControllerProtocol? private weak var gesture: ContextGesture? private var displayTextSelectionTip: Bool @@ -1159,7 +1166,6 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi if let layout = self.validLayout { self.updateLayout(layout: layout, transition: .animated(duration: 0.3, curve: .spring), previousActionsContainerNode: previousActionsContainerNode) - } else { previousActionsContainerNode.removeFromSupernode() } @@ -1748,7 +1754,7 @@ public enum ContextContentSource { case controller(ContextControllerContentSource) } -public final class ContextController: ViewController, StandalonePresentableController { +public final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol { private let account: Account private var presentationData: PresentationData private let source: ContextContentSource diff --git a/submodules/Display/Source/PeekController.swift b/submodules/ContextUI/Sources/PeekController.swift similarity index 70% rename from submodules/Display/Source/PeekController.swift rename to submodules/ContextUI/Sources/PeekController.swift index 69af3b1505..c5483234cc 100644 --- a/submodules/Display/Source/PeekController.swift +++ b/submodules/ContextUI/Sources/PeekController.swift @@ -1,6 +1,9 @@ import Foundation import UIKit import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramPresentationData public final class PeekControllerTheme { public let isDark: Bool @@ -20,12 +23,25 @@ public final class PeekControllerTheme { } } -public final class PeekController: ViewController { +extension PeekControllerTheme { + convenience public init(presentationTheme: PresentationTheme) { + let actionSheet = presentationTheme.actionSheet + self.init(isDark: actionSheet.backgroundType == .dark, menuBackgroundColor: actionSheet.opaqueItemBackgroundColor, menuItemHighligtedColor: actionSheet.opaqueItemHighlightedBackgroundColor, menuItemSeparatorColor: actionSheet.opaqueItemSeparatorColor, accentColor: actionSheet.controlAccentColor, destructiveColor: actionSheet.destructiveActionTextColor) + } +} + +public final class PeekController: ViewController, ContextControllerProtocol { + public var useComplexItemsTransitionAnimation: Bool = false + + public func setItems(_ items: Signal<[ContextMenuItem], NoError>) { + + } + private var controllerNode: PeekControllerNode { return self.displayNode as! PeekControllerNode } - private let theme: PeekControllerTheme + private let presentationData: PresentationData private let content: PeekControllerContent var sourceNode: () -> ASDisplayNode? @@ -33,8 +49,8 @@ public final class PeekController: ViewController { private var animatedIn = false - public init(theme: PeekControllerTheme, content: PeekControllerContent, sourceNode: @escaping () -> ASDisplayNode?) { - self.theme = theme + public init(presentationData: PresentationData, content: PeekControllerContent, sourceNode: @escaping () -> ASDisplayNode?) { + self.presentationData = presentationData self.content = content self.sourceNode = sourceNode @@ -48,7 +64,7 @@ public final class PeekController: ViewController { } override public func loadDisplayNode() { - self.displayNode = PeekControllerNode(theme: self.theme, content: self.content, requestDismiss: { [weak self] in + self.displayNode = PeekControllerNode(presentationData: self.presentationData, controller: self, content: self.content, requestDismiss: { [weak self] in self?.dismiss() }) self.displayNodeDidLoad() diff --git a/submodules/Display/Source/PeekControllerContent.swift b/submodules/ContextUI/Sources/PeekControllerContent.swift similarity index 78% rename from submodules/Display/Source/PeekControllerContent.swift rename to submodules/ContextUI/Sources/PeekControllerContent.swift index c2767e87c6..e1be451d53 100644 --- a/submodules/Display/Source/PeekControllerContent.swift +++ b/submodules/ContextUI/Sources/PeekControllerContent.swift @@ -1,21 +1,22 @@ import Foundation import UIKit import AsyncDisplayKit +import Display public enum PeekControllerContentPresentation { case contained case freeform } -public enum PeerkControllerMenuActivation { +public enum PeerControllerMenuActivation { case drag case press } public protocol PeekControllerContent { func presentation() -> PeekControllerContentPresentation - func menuActivation() -> PeerkControllerMenuActivation - func menuItems() -> [PeekControllerMenuItem] + func menuActivation() -> PeerControllerMenuActivation + func menuItems() -> [ContextMenuItem] func node() -> PeekControllerContentNode & ASDisplayNode func topAccessoryNode() -> ASDisplayNode? diff --git a/submodules/Display/Source/PeekControllerGestureRecognizer.swift b/submodules/ContextUI/Sources/PeekControllerGestureRecognizer.swift similarity index 93% rename from submodules/Display/Source/PeekControllerGestureRecognizer.swift rename to submodules/ContextUI/Sources/PeekControllerGestureRecognizer.swift index abaa6c0201..ab4a8a323f 100644 --- a/submodules/Display/Source/PeekControllerGestureRecognizer.swift +++ b/submodules/ContextUI/Sources/PeekControllerGestureRecognizer.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import SwiftSignalKit import AsyncDisplayKit +import Display private func traceDeceleratingScrollView(_ view: UIView, at point: CGPoint) -> Bool { if view.bounds.contains(point), let view = view as? UIScrollView, view.isDecelerating { @@ -33,7 +34,7 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { } } - private var menuActivation: PeerkControllerMenuActivation? + private var menuActivation: PeerControllerMenuActivation? private weak var presentedController: PeekController? public init(contentAtPoint: @escaping (CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>?, present: @escaping (PeekControllerContent, ASDisplayNode) -> ViewController?, updateContent: @escaping (PeekControllerContent?) -> Void = { _ in }, activateBySingleTap: Bool = false) { @@ -105,8 +106,8 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { (presentedController.displayNode as? PeekControllerNode)?.activateMenu() } self.menuActivation = nil - self.presentedController = nil - self.state = .ended +// self.presentedController = nil +// self.state = .ended } } } @@ -136,10 +137,8 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { } self.state = .ended } else { - let velocity = self.velocity(in: self.view) - - if let presentedController = self.presentedController, presentedController.isNodeLoaded { - (presentedController.displayNode as? PeekControllerNode)?.endDraggingWithVelocity(velocity.y) + if let presentedController = self.presentedController, presentedController.isNodeLoaded, let location = touches.first?.location(in: presentedController.view) { + (presentedController.displayNode as? PeekControllerNode)?.endDragging(location) self.presentedController = nil self.menuActivation = nil } @@ -172,7 +171,12 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { if let touch = touches.first, let initialTapLocation = self.tapLocation { let touchLocation = touch.location(in: self.view) - if let menuActivation = self.menuActivation, let presentedController = self.presentedController { + if let presentedController = self.presentedController, self.menuActivation == nil { + if presentedController.isNodeLoaded { + let touchLocation = touch.location(in: presentedController.view) + (presentedController.displayNode as? PeekControllerNode)?.applyDraggingOffset(touchLocation) + } + } else if let menuActivation = self.menuActivation, let presentedController = self.presentedController { switch menuActivation { case .drag: var offset = touchLocation.y - initialTapLocation.y @@ -181,7 +185,7 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0) if presentedController.isNodeLoaded { - (presentedController.displayNode as? PeekControllerNode)?.applyDraggingOffset(offset) +// (presentedController.displayNode as? PeekControllerNode)?.applyDraggingOffset(offset) } case .press: if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { diff --git a/submodules/ContextUI/Sources/PeekControllerNode.swift b/submodules/ContextUI/Sources/PeekControllerNode.swift new file mode 100644 index 0000000000..da15c5a6d2 --- /dev/null +++ b/submodules/ContextUI/Sources/PeekControllerNode.swift @@ -0,0 +1,346 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData + +private let animationDurationFactor: Double = 1.0 + +final class PeekControllerNode: ViewControllerTracingNode { + private let requestDismiss: () -> Void + + private let presentationData: PresentationData + private let theme: PeekControllerTheme + + private weak var controller: PeekController? + + private let blurView: UIView + private let dimNode: ASDisplayNode + private let containerBackgroundNode: ASImageNode + private let containerNode: ASDisplayNode + private let darkDimNode: ASDisplayNode + + private var validLayout: ContainerViewLayout? + + private var content: PeekControllerContent + private var contentNode: PeekControllerContentNode & ASDisplayNode + private var contentNodeHasValidLayout = false + + private var topAccessoryNode: ASDisplayNode? + + private var actionsContainerNode: ContextActionsContainerNode + + private var hapticFeedback = HapticFeedback() + + private var initialContinueGesturePoint: CGPoint? + private var didMoveFromInitialGesturePoint = false + private var highlightedActionNode: ContextActionNodeProtocol? + + init(presentationData: PresentationData, controller: PeekController, content: PeekControllerContent, requestDismiss: @escaping () -> Void) { + self.presentationData = presentationData + self.requestDismiss = requestDismiss + self.theme = PeekControllerTheme(presentationTheme: presentationData.theme) + self.controller = controller + + self.dimNode = ASDisplayNode() + self.blurView = UIVisualEffectView(effect: UIBlurEffect(style: self.theme.isDark ? .dark : .light)) + self.blurView.isUserInteractionEnabled = false + + self.darkDimNode = ASDisplayNode() + self.darkDimNode.alpha = 0.0 + self.darkDimNode.backgroundColor = presentationData.theme.contextMenu.dimColor + self.darkDimNode.isUserInteractionEnabled = false + + switch content.menuActivation() { + case .drag: + self.dimNode.backgroundColor = nil + self.blurView.alpha = 1.0 + case .press: + self.dimNode.backgroundColor = UIColor(white: self.theme.isDark ? 0.0 : 1.0, alpha: 0.5) + self.blurView.alpha = 0.0 + } + + self.containerBackgroundNode = ASImageNode() + self.containerBackgroundNode.isLayerBacked = true + self.containerBackgroundNode.displaysAsynchronously = false + + self.containerNode = ASDisplayNode() + + self.content = content + self.contentNode = content.node() + self.topAccessoryNode = content.topAccessoryNode() + + var feedbackTapImpl: (() -> Void)? + var activatedActionImpl: (() -> Void)? + self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: content.menuItems(), getController: { [weak controller] in + return controller + }, actionSelected: { result in + activatedActionImpl?() + }, feedbackTap: { + feedbackTapImpl?() + }, displayTextSelectionTip: false, blurBackground: true) + self.actionsContainerNode.alpha = 0.0 + + super.init() + + feedbackTapImpl = { [weak self] in + self?.hapticFeedback.tap() + } + + if content.presentation() == .freeform { + self.containerNode.isUserInteractionEnabled = false + } else { + self.containerNode.clipsToBounds = true + self.containerNode.cornerRadius = 16.0 + } + + self.addSubnode(self.dimNode) + self.view.addSubview(self.blurView) + self.addSubnode(self.darkDimNode) + self.containerNode.addSubnode(self.contentNode) + + self.addSubnode(self.actionsContainerNode) + self.addSubnode(self.containerNode) + + activatedActionImpl = { [weak self] in + self?.requestDismiss() + } + + self.hapticFeedback.prepareTap() + } + + deinit { + } + + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:)))) + self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.darkDimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(view: self.blurView, frame: CGRect(origin: CGPoint(), size: layout.size)) + + var layoutInsets = layout.insets(options: []) + let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left) + + layoutInsets.left = floor((layout.size.width - containerWidth) / 2.0) + layoutInsets.right = layoutInsets.left + if !layoutInsets.bottom.isZero { + layoutInsets.bottom -= 12.0 + } + + let maxContainerSize = CGSize(width: layout.size.width - 14.0 * 2.0, height: layout.size.height - layoutInsets.top - layoutInsets.bottom - 90.0) + + let contentSize = self.contentNode.updateLayout(size: maxContainerSize, transition: self.contentNodeHasValidLayout ? transition : .immediate) + if self.contentNodeHasValidLayout { + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize)) + } else { + self.contentNode.frame = CGRect(origin: CGPoint(), size: contentSize) + } + + let actionsSideInset: CGFloat = layout.safeInsets.left + 11.0 + let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, transition: .immediate) + + let containerFrame: CGRect + let actionsFrame: CGRect + if layout.size.width > layout.size.height { + if self.actionsContainerNode.alpha.isZero { + containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) + } else { + containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 3.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) + } + actionsFrame = CGRect(origin: CGPoint(x: containerFrame.maxX + 32.0, y: floor((layout.size.height - actionsSize.height) / 2.0)), size: actionsSize) + } else { + switch self.content.presentation() { + case .contained: + containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) + case .freeform: + containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 3.0)), size: contentSize) + } + actionsFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - actionsSize.width) / 2.0), y: containerFrame.maxY + 32.0), size: actionsSize) + } + transition.updateFrame(node: self.containerNode, frame: containerFrame) + + self.actionsContainerNode.updateSize(containerSize: actionsSize, contentSize: actionsSize) + transition.updateFrame(node: self.actionsContainerNode, frame: actionsFrame) + + self.contentNodeHasValidLayout = true + } + + func animateIn(from rect: CGRect) { + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.blurView.layer.animateAlpha(from: 0.0, to: self.blurView.alpha, duration: 0.3) + + let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y) + self.containerNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true) + self.containerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0) + self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + + if let topAccessoryNode = self.topAccessoryNode { + topAccessoryNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true) + topAccessoryNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0) + topAccessoryNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + if case .press = self.content.menuActivation() { + self.hapticFeedback.tap() + } else { + self.hapticFeedback.impact() + } + } + + func animateOut(to rect: CGRect, completion: @escaping () -> Void) { + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.blurView.layer.animateAlpha(from: self.blurView.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.darkDimNode.layer.animateAlpha(from: self.darkDimNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + + let springDuration: Double = 0.42 * animationDurationFactor + let springDamping: CGFloat = 104.0 + + let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y) + self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: offset), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { _ in + completion() + }) + self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.containerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false) + + if !self.actionsContainerNode.alpha.isZero { + let actionsOffset = CGPoint(x: rect.midX - self.actionsContainerNode.position.x, y: rect.midY - self.actionsContainerNode.position.y) + self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false) + self.actionsContainerNode.layer.animateSpring(from: 1.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false) + self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: actionsOffset), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + } + } + + @objc func dimNodeTap(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.requestDismiss() + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + guard case .drag = self.content.menuActivation() else { + return + } + + let location = recognizer.location(in: self.view) + switch recognizer.state { + case .began: + break + case .changed: + self.applyDraggingOffset(location) + case .cancelled, .ended: + self.endDragging(location) + default: + break + } + } + + func applyDraggingOffset(_ offset: CGPoint) { + let localPoint = offset + let initialPoint: CGPoint + if let current = self.initialContinueGesturePoint { + initialPoint = current + } else { + initialPoint = localPoint + self.initialContinueGesturePoint = localPoint + } + if !self.actionsContainerNode.alpha.isZero { + if !self.didMoveFromInitialGesturePoint { + let distance = abs(localPoint.y - initialPoint.y) + if distance > 12.0 { + self.didMoveFromInitialGesturePoint = true + } + } + if self.didMoveFromInitialGesturePoint { + let actionPoint = self.view.convert(localPoint, to: self.actionsContainerNode.view) + let actionNode = self.actionsContainerNode.actionNode(at: actionPoint) + if self.highlightedActionNode !== actionNode { + self.highlightedActionNode?.setIsHighlighted(false) + self.highlightedActionNode = actionNode + if let actionNode = actionNode { + actionNode.setIsHighlighted(true) + self.hapticFeedback.tap() + } + } + } + } + } + + func activateMenu() { + if case .press = self.content.menuActivation() { + self.hapticFeedback.impact() + } + + let springDuration: Double = 0.42 * animationDurationFactor + let springDamping: CGFloat = 104.0 + + let previousBlurAlpha = self.blurView.alpha + self.blurView.alpha = 1.0 + self.blurView.layer.animateAlpha(from: previousBlurAlpha, to: self.blurView.alpha, duration: 0.3) + + let previousDarkDimAlpha = self.darkDimNode.alpha + self.darkDimNode.alpha = 1.0 + self.darkDimNode.layer.animateAlpha(from: previousDarkDimAlpha, to: 1.0, duration: 0.3) + + self.actionsContainerNode.alpha = 1.0 + self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) + self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + + let localContentSourceFrame = self.containerNode.frame + self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localContentSourceFrame.center.x - self.actionsContainerNode.position.x, y: localContentSourceFrame.center.y - self.actionsContainerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: .animated(duration: springDuration, curve: .spring)) + } + } + + func endDragging(_ location: CGPoint) { + if self.didMoveFromInitialGesturePoint { + if let highlightedActionNode = self.highlightedActionNode { + self.highlightedActionNode = nil + highlightedActionNode.performAction() + } + } + } + + func updateContent(content: PeekControllerContent) { + let contentNode = self.contentNode + contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak contentNode] _ in + contentNode?.removeFromSupernode() + }) + contentNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.15, removeOnCompletion: false) + + self.content = content + self.contentNode = content.node() + self.containerNode.addSubnode(self.contentNode) + self.contentNodeHasValidLayout = false + + let previousActionsContainerNode = self.actionsContainerNode + self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: content.menuItems(), getController: { [weak self] in + return self?.controller + }, actionSelected: { [weak self] result in + self?.requestDismiss() + }, feedbackTap: { [weak self] in + self?.hapticFeedback.tap() + }, displayTextSelectionTip: false, blurBackground: true) + self.actionsContainerNode.alpha = 0.0 + self.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode) + previousActionsContainerNode.removeFromSupernode() + + self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + self.contentNode.layer.animateSpring(from: 0.35 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: .animated(duration: 0.15, curve: .easeInOut)) + } + + self.hapticFeedback.tap() + } +} diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 2d9c00ddd8..1ce169edb8 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -182,6 +182,39 @@ public extension ContainedViewLayoutTransition { } } + func updateFrameAsPositionAndBounds(layer: CALayer, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { + if layer.frame.equalTo(frame) && !force { + completion?(true) + } else { + switch self { + case .immediate: + layer.position = frame.center + layer.bounds = CGRect(origin: CGPoint(), size: frame.size) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousPosition: CGPoint + let previousBounds: CGRect + if beginWithCurrentState, let presentation = layer.presentation() { + previousPosition = presentation.position + previousBounds = presentation.bounds + } else { + previousPosition = layer.position + previousBounds = layer.bounds + } + layer.position = frame.center + layer.bounds = CGRect(origin: CGPoint(), size: frame.size) + layer.animateFrame(from: + CGRect(origin: CGPoint(x: previousPosition.x - previousBounds.width / 2.0, y: previousPosition.y - previousBounds.height / 2.0), size: previousBounds.size), to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + } + func updateFrameAdditive(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.frame.equalTo(frame) && !force { completion?(true) diff --git a/submodules/Display/Source/PeekControllerMenuItemNode.swift b/submodules/Display/Source/PeekControllerMenuItemNode.swift deleted file mode 100644 index c1c872986e..0000000000 --- a/submodules/Display/Source/PeekControllerMenuItemNode.swift +++ /dev/null @@ -1,108 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit - -public enum PeekControllerMenuItemColor { - case accent - case destructive -} - -public enum PeekControllerMenuItemFont { - case `default` - case bold -} - -public struct PeekControllerMenuItem { - public let title: String - public let color: PeekControllerMenuItemColor - public let font: PeekControllerMenuItemFont - public let action: (ASDisplayNode, CGRect) -> Bool - - public init(title: String, color: PeekControllerMenuItemColor, font: PeekControllerMenuItemFont = .default, action: @escaping (ASDisplayNode, CGRect) -> Bool) { - self.title = title - self.color = color - self.font = font - self.action = action - } -} - -final class PeekControllerMenuItemNode: HighlightTrackingButtonNode { - private let item: PeekControllerMenuItem - private let activatedAction: () -> Void - - private let separatorNode: ASDisplayNode - private let highlightedBackgroundNode: ASDisplayNode - private let textNode: ImmediateTextNode - - init(theme: PeekControllerTheme, item: PeekControllerMenuItem, activatedAction: @escaping () -> Void) { - self.item = item - self.activatedAction = activatedAction - - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - self.separatorNode.backgroundColor = theme.menuItemSeparatorColor - - self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.isLayerBacked = true - self.highlightedBackgroundNode.backgroundColor = theme.menuItemHighligtedColor - self.highlightedBackgroundNode.alpha = 0.0 - - self.textNode = ImmediateTextNode() - self.textNode.isUserInteractionEnabled = false - self.textNode.displaysAsynchronously = false - - let textColor: UIColor - let textFont: UIFont - switch item.color { - case .accent: - textColor = theme.accentColor - case .destructive: - textColor = theme.destructiveColor - } - switch item.font { - case .default: - textFont = Font.regular(20.0) - case .bold: - textFont = Font.medium(20.0) - } - self.textNode.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: textColor) - - super.init() - - self.addSubnode(self.separatorNode) - self.addSubnode(self.highlightedBackgroundNode) - self.addSubnode(self.textNode) - - self.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.view.superview?.bringSubviewToFront(strongSelf.view) - strongSelf.highlightedBackgroundNode.alpha = 1.0 - } else { - strongSelf.highlightedBackgroundNode.alpha = 0.0 - strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - } - } - } - - self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - } - - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let height: CGFloat = 57.0 - transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height), size: CGSize(width: width, height: UIScreenPixel))) - - let textSize = self.textNode.updateLayout(CGSize(width: width - 10.0, height: height)) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((width - textSize.width) / 2.0), y: floor((height - textSize.height) / 2.0)), size: textSize)) - - return height - } - - @objc func buttonPressed() { - self.activatedAction() - if self.item.action(self, self.bounds) { - - } - } -} diff --git a/submodules/Display/Source/PeekControllerMenuNode.swift b/submodules/Display/Source/PeekControllerMenuNode.swift deleted file mode 100644 index 6b7e5c4fdb..0000000000 --- a/submodules/Display/Source/PeekControllerMenuNode.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit - -final class PeekControllerMenuNode: ASDisplayNode { - private let itemNodes: [PeekControllerMenuItemNode] - - init(theme: PeekControllerTheme, items: [PeekControllerMenuItem], activatedAction: @escaping () -> Void) { - self.itemNodes = items.map { PeekControllerMenuItemNode(theme: theme, item: $0, activatedAction: activatedAction) } - - super.init() - - self.backgroundColor = theme.menuBackgroundColor - self.cornerRadius = 16.0 - self.clipsToBounds = true - - for itemNode in self.itemNodes { - self.addSubnode(itemNode) - } - } - - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - var verticalOffset: CGFloat = 0.0 - for itemNode in self.itemNodes { - let itemHeight = itemNode.updateLayout(width: width, transition: transition) - transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: width, height: itemHeight))) - verticalOffset += itemHeight - } - return verticalOffset - UIScreenPixel - } -} diff --git a/submodules/Display/Source/PeekControllerNode.swift b/submodules/Display/Source/PeekControllerNode.swift deleted file mode 100644 index fabe9c53b9..0000000000 --- a/submodules/Display/Source/PeekControllerNode.swift +++ /dev/null @@ -1,357 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit - -final class PeekControllerNode: ViewControllerTracingNode { - private let requestDismiss: () -> Void - - private let theme: PeekControllerTheme - - private let blurView: UIView - private let dimNode: ASDisplayNode - private let containerBackgroundNode: ASImageNode - private let containerNode: ASDisplayNode - - private var validLayout: ContainerViewLayout? - private var containerOffset: CGFloat = 0.0 - private var panInitialContainerOffset: CGFloat? - - private var content: PeekControllerContent - private var contentNode: PeekControllerContentNode & ASDisplayNode - private var contentNodeHasValidLayout = false - - private var topAccessoryNode: ASDisplayNode? - - private var menuNode: PeekControllerMenuNode? - private var displayingMenu = false - - private var hapticFeedback: HapticFeedback? - - init(theme: PeekControllerTheme, content: PeekControllerContent, requestDismiss: @escaping () -> Void) { - self.theme = theme - self.requestDismiss = requestDismiss - - self.dimNode = ASDisplayNode() - self.blurView = UIVisualEffectView(effect: UIBlurEffect(style: theme.isDark ? .dark : .light)) - self.blurView.isUserInteractionEnabled = false - - switch content.menuActivation() { - case .drag: - self.dimNode.backgroundColor = nil - self.blurView.alpha = 1.0 - case .press: - self.dimNode.backgroundColor = UIColor(white: theme.isDark ? 0.0 : 1.0, alpha: 0.5) - self.blurView.alpha = 0.0 - } - - self.containerBackgroundNode = ASImageNode() - self.containerBackgroundNode.isLayerBacked = true - self.containerBackgroundNode.displaysAsynchronously = false - - self.containerNode = ASDisplayNode() - - self.content = content - self.contentNode = content.node() - self.topAccessoryNode = content.topAccessoryNode() - - var activatedActionImpl: (() -> Void)? - let menuItems = content.menuItems() - if menuItems.isEmpty { - self.menuNode = nil - } else { - self.menuNode = PeekControllerMenuNode(theme: theme, items: menuItems, activatedAction: { - activatedActionImpl?() - }) - } - - super.init() - - if content.presentation() == .freeform { - self.containerNode.isUserInteractionEnabled = false - } else { - self.containerNode.clipsToBounds = true - self.containerNode.cornerRadius = 16.0 - } - - self.addSubnode(self.dimNode) - self.view.addSubview(self.blurView) - self.containerNode.addSubnode(self.contentNode) - self.addSubnode(self.containerNode) - - if let topAccessoryNode = self.topAccessoryNode { - self.addSubnode(topAccessoryNode) - } - - if let menuNode = self.menuNode { - self.addSubnode(menuNode) - } - - activatedActionImpl = { [weak self] in - self?.requestDismiss() - } - - self.hapticFeedback = HapticFeedback() - self.hapticFeedback?.prepareTap() - } - - deinit { - } - - override func didLoad() { - super.didLoad() - - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:)))) - self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.validLayout = layout - - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(view: self.blurView, frame: CGRect(origin: CGPoint(), size: layout.size)) - - var layoutInsets = layout.insets(options: []) - let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left) - - layoutInsets.left = floor((layout.size.width - containerWidth) / 2.0) - layoutInsets.right = layoutInsets.left - if !layoutInsets.bottom.isZero { - layoutInsets.bottom -= 12.0 - } - - let maxContainerSize = CGSize(width: layout.size.width - 14.0 * 2.0, height: layout.size.height - layoutInsets.top - layoutInsets.bottom - 90.0) - - var menuSize: CGSize? - - let contentSize = self.contentNode.updateLayout(size: maxContainerSize, transition: self.contentNodeHasValidLayout ? transition : .immediate) - if self.contentNodeHasValidLayout { - transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize)) - } else { - self.contentNode.frame = CGRect(origin: CGPoint(), size: contentSize) - } - - var containerFrame: CGRect - switch self.content.presentation() { - case .contained: - containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) - case .freeform: - containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 4.0)), size: contentSize) - } - - if let menuNode = self.menuNode { - let menuWidth = layout.size.width - layoutInsets.left - layoutInsets.right - 14.0 * 2.0 - let menuHeight = menuNode.updateLayout(width: menuWidth, transition: transition) - menuSize = CGSize(width: menuWidth, height: menuHeight) - - if self.displayingMenu { - let upperBound = layout.size.height - layoutInsets.bottom - menuHeight - 14.0 * 2.0 - containerFrame.height - if containerFrame.origin.y > upperBound { - containerFrame.origin.y = upperBound - } - - transition.updateAlpha(layer: self.blurView.layer, alpha: 1.0) - } - } - - if self.displayingMenu { - var offset = self.containerOffset - let delta = abs(offset) - let factor: CGFloat = 60.0 - offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0) - containerFrame = containerFrame.offsetBy(dx: 0.0, dy: offset) - } else { - containerFrame = containerFrame.offsetBy(dx: 0.0, dy: self.containerOffset) - } - - transition.updateFrame(node: self.containerNode, frame: containerFrame) - - if let topAccessoryNode = self.topAccessoryNode { - let accessorySize = topAccessoryNode.frame.size - let accessoryFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(containerFrame.midX - accessorySize.width / 2.0), y: containerFrame.minY - accessorySize.height - 16.0), size: accessorySize) - transition.updateFrame(node: topAccessoryNode, frame: accessoryFrame) - transition.updateAlpha(node: topAccessoryNode, alpha: self.displayingMenu ? 0.0 : 1.0) - } - - if let menuNode = self.menuNode, let menuSize = menuSize { - let menuY: CGFloat - if self.displayingMenu { - menuY = max(containerFrame.maxY + 14.0, layout.size.height - layoutInsets.bottom - 14.0 - menuSize.height) - } else { - menuY = layout.size.height + 14.0 - } - - let menuFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - menuSize.width) / 2.0), y: menuY), size: menuSize) - - if self.contentNodeHasValidLayout { - transition.updateFrame(node: menuNode, frame: menuFrame) - } else { - menuNode.frame = menuFrame - } - } - - self.contentNodeHasValidLayout = true - } - - func animateIn(from rect: CGRect) { - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.blurView.layer.animateAlpha(from: 0.0, to: self.blurView.alpha, duration: 0.3) - - let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y) - self.containerNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true) - self.containerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0) - self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - - if let topAccessoryNode = self.topAccessoryNode { - topAccessoryNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true) - topAccessoryNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0) - topAccessoryNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - } - - if case .press = self.content.menuActivation() { - self.hapticFeedback?.tap() - } else { - self.hapticFeedback?.impact() - } - } - - func animateOut(to rect: CGRect, completion: @escaping () -> Void) { - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.blurView.layer.animateAlpha(from: self.blurView.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false) - - let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y) - self.containerNode.layer.animatePosition(from: CGPoint(), to: offset, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, force: true, completion: { _ in - completion() - }) - self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.containerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false) - - if let topAccessoryNode = self.topAccessoryNode { - topAccessoryNode.layer.animatePosition(from: CGPoint(), to: offset, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, force: true, completion: { _ in - completion() - }) - topAccessoryNode.layer.animateAlpha(from: topAccessoryNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) - topAccessoryNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false) - } - - if let menuNode = self.menuNode { - menuNode.layer.animatePosition(from: menuNode.position, to: CGPoint(x: menuNode.position.x, y: self.bounds.size.height + menuNode.bounds.size.height / 2.0), duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) - } - } - - @objc func dimNodeTap(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.requestDismiss() - } - } - - @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { - guard case .drag = self.content.menuActivation() else { - return - } - - switch recognizer.state { - case .began: - self.panInitialContainerOffset = self.containerOffset - case .changed: - if let panInitialContainerOffset = self.panInitialContainerOffset { - let translation = recognizer.translation(in: self.view) - var offset = panInitialContainerOffset + translation.y - if offset < 0.0 { - let delta = abs(offset) - let factor: CGFloat = 60.0 - offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0) - } - self.applyDraggingOffset(offset) - } - case .cancelled, .ended: - if let _ = self.panInitialContainerOffset { - self.panInitialContainerOffset = nil - if self.containerOffset < 0.0 { - self.activateMenu() - } else { - self.requestDismiss() - } - } - default: - break - } - } - - func applyDraggingOffset(_ offset: CGFloat) { - self.containerOffset = offset - if self.containerOffset < -25.0 { - //self.displayingMenu = true - } else { - //self.displayingMenu = false - } - if let layout = self.validLayout { - self.containerLayoutUpdated(layout, transition: .immediate) - } - } - - func activateMenu() { - if case .press = self.content.menuActivation() { - self.hapticFeedback?.impact() - } - if let layout = self.validLayout { - self.displayingMenu = true - self.containerOffset = 0.0 - self.containerLayoutUpdated(layout, transition: .animated(duration: 0.18, curve: .spring)) - } - } - - func endDraggingWithVelocity(_ velocity: CGFloat) { - if let _ = self.menuNode, velocity < -600.0 || self.containerOffset < -38.0 { - if let layout = self.validLayout { - self.displayingMenu = true - self.containerOffset = 0.0 - self.containerLayoutUpdated(layout, transition: .animated(duration: 0.18, curve: .spring)) - } - } else { - self.requestDismiss() - } - } - - func updateContent(content: PeekControllerContent) { - let contentNode = self.contentNode - contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak contentNode] _ in - contentNode?.removeFromSupernode() - }) - contentNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.15, removeOnCompletion: false) - - self.menuNode?.removeFromSupernode() - self.menuNode = nil - - self.content = content - self.contentNode = content.node() - self.containerNode.addSubnode(self.contentNode) - self.contentNodeHasValidLayout = false - - var activatedActionImpl: (() -> Void)? - let menuItems = content.menuItems() - if menuItems.isEmpty { - self.menuNode = nil - } else { - self.menuNode = PeekControllerMenuNode(theme: self.theme, items: menuItems, activatedAction: { - activatedActionImpl?() - }) - } - - if let menuNode = self.menuNode { - self.addSubnode(menuNode) - } - - activatedActionImpl = { [weak self] in - self?.requestDismiss() - } - - self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) - self.contentNode.layer.animateSpring(from: 0.35 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) - - if let layout = self.validLayout { - self.containerLayoutUpdated(layout, transition: .animated(duration: 0.15, curve: .easeInOut)) - } - - self.hapticFeedback?.tap() - } -} diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index d4e36bbd54..2ecff6f2c3 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -183,8 +183,6 @@ public enum TabBarItemContextActionType { public let navigationBar: NavigationBar? private(set) var toolbar: Toolbar? - private var previewingContext: Any? - public var displayNavigationBar = true open var navigationBarRequiresEntireLayoutUpdate: Bool { return true @@ -612,33 +610,6 @@ public enum TabBarItemContextActionType { } } - @available(iOSApplicationExtension 9.0, iOS 9.0, *) - open func registerForPreviewing(with delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, theme: PeekControllerTheme, onlyNative: Bool) { - } - - @available(iOSApplicationExtension 9.0, iOS 9.0, *) - public func registerForPreviewingNonNative(with delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, theme: PeekControllerTheme) { - if true || self.traitCollection.forceTouchCapability != .available { - if self.previewingContext == nil { - let previewingContext = SimulatedViewControllerPreviewing(theme: theme, delegate: delegate, sourceView: sourceView, node: self.displayNode, present: { [weak self] c, a in - self?.presentInGlobalOverlay(c, with: a) - }, customPresent: { [weak self] c, n in - return self?.customPresentPreviewingController?(c, n) - }) - self.previewingContext = previewingContext - } - } - } - - @available(iOSApplicationExtension 9.0, iOS 9.0, *) - open override func unregisterForPreviewing(withContext previewing: UIViewControllerPreviewing) { - if self.previewingContext != nil { - self.previewingContext = nil - } else { - super.unregisterForPreviewing(withContext: previewing) - } - } - public final func navigationNextSibling() -> UIViewController? { if let navigationController = self.navigationController as? NavigationController { if let index = navigationController.viewControllers.firstIndex(where: { $0 === self }) { diff --git a/submodules/Display/Source/ViewControllerPreviewing.swift b/submodules/Display/Source/ViewControllerPreviewing.swift deleted file mode 100644 index f9f8072015..0000000000 --- a/submodules/Display/Source/ViewControllerPreviewing.swift +++ /dev/null @@ -1,137 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import SwiftSignalKit - -@available(iOSApplicationExtension 9.0, iOS 9.0, *) -private final class ViewControllerPeekContent: PeekControllerContent { - let controller: ViewController - private let menu: [PeekControllerMenuItem] - - init(controller: ViewController) { - self.controller = controller - var menu: [PeekControllerMenuItem] = [] - for item in controller.previewActionItems { - menu.append(PeekControllerMenuItem(title: item.title, color: .accent, action: { [weak controller] _, _ in - if let controller = controller, let item = item as? UIPreviewAction { - item.handler(item, controller) - } - return true - })) - } - self.menu = menu - } - - func presentation() -> PeekControllerContentPresentation { - return .contained - } - - func menuActivation() -> PeerkControllerMenuActivation { - return .drag - } - - func menuItems() -> [PeekControllerMenuItem] { - return self.menu - } - - func node() -> PeekControllerContentNode & ASDisplayNode { - return ViewControllerPeekContentNode(controller: self.controller) - } - - func topAccessoryNode() -> ASDisplayNode? { - return nil - } - - func isEqual(to: PeekControllerContent) -> Bool { - if let to = to as? ViewControllerPeekContent { - return self.controller === to.controller - } else { - return false - } - } -} - -private final class ViewControllerPeekContentNode: ASDisplayNode, PeekControllerContentNode { - private let controller: ViewController - private var hasValidLayout = false - - init(controller: ViewController) { - self.controller = controller - - super.init() - } - - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - if !self.hasValidLayout { - self.hasValidLayout = true - self.controller.view.frame = CGRect(origin: CGPoint(), size: size) - self.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(), deviceMetrics: .unknown(screenSize: size, statusBarHeight: 20.0, onScreenNavigationHeight: nil), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate) - self.controller.setIgnoreAppearanceMethodInvocations(true) - self.view.addSubview(self.controller.view) - self.controller.setIgnoreAppearanceMethodInvocations(false) - self.controller.viewWillAppear(false) - self.controller.viewDidAppear(false) - } else { - self.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(), deviceMetrics: .unknown(screenSize: size, statusBarHeight: 20.0, onScreenNavigationHeight: nil), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) - } - - return size - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.bounds.contains(point) { - return self.view - } - return nil - } -} - -@available(iOSApplicationExtension 9.0, iOS 9.0, *) -final class SimulatedViewControllerPreviewing: NSObject, UIViewControllerPreviewing { - weak var delegateImpl: UIViewControllerPreviewingDelegate? - var delegate: UIViewControllerPreviewingDelegate { - return self.delegateImpl! - } - let recognizer: PeekControllerGestureRecognizer - var previewingGestureRecognizerForFailureRelationship: UIGestureRecognizer { - return self.recognizer - } - let sourceView: UIView - let node: ASDisplayNode - - var sourceRect: CGRect = CGRect() - - init(theme: PeekControllerTheme, delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, node: ASDisplayNode, present: @escaping (ViewController, Any?) -> Void, customPresent: ((ViewController, ASDisplayNode) -> ViewController?)?) { - self.delegateImpl = delegate - self.sourceView = sourceView - self.node = node - var contentAtPointImpl: ((CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>?)? - self.recognizer = PeekControllerGestureRecognizer(contentAtPoint: { point in - return contentAtPointImpl?(point) - }, present: { content, sourceNode in - if let content = content as? ViewControllerPeekContent, let controller = customPresent?(content.controller, sourceNode) { - present(controller, nil) - return controller - } else { - let controller = PeekController(theme: theme, content: content, sourceNode: { - return sourceNode - }) - present(controller, nil) - return controller - } - }) - - node.view.addGestureRecognizer(self.recognizer) - - super.init() - - contentAtPointImpl = { [weak self] point in - if let strongSelf = self, let delegate = strongSelf.delegateImpl { - if let controller = delegate.previewingContext(strongSelf, viewControllerForLocation: point) as? ViewController { - return .single((strongSelf.node, ViewControllerPeekContent(controller: controller))) - } - } - return nil - } - } -} diff --git a/submodules/ImportStickerPackUI/BUILD b/submodules/ImportStickerPackUI/BUILD index c515f3d26b..b6e9b9e862 100644 --- a/submodules/ImportStickerPackUI/BUILD +++ b/submodules/ImportStickerPackUI/BUILD @@ -17,7 +17,7 @@ swift_library( "//submodules/AccountContext:AccountContext", "//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/ShareController:ShareController", - "//submodules/ItemListUI:ItemListUI", + "//submodules/ItemListUI:ItemListUI", "//submodules/StickerResources:StickerResources", "//submodules/AlertUI:AlertUI", "//submodules/PresentationDataUtils:PresentationDataUtils", @@ -28,6 +28,7 @@ swift_library( "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/UndoUI:UndoUI", + "//submodules/ContextUI:ContextUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift index 6a50c2459d..565fa76406 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift @@ -12,6 +12,7 @@ import MergeLists import ActivityIndicator import TextFormat import AccountContext +import ContextUI private struct StickerPackPreviewGridEntry: Comparable, Identifiable { let index: Int @@ -189,7 +190,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.presentationData.theme), content: content, sourceNode: { + let controller = PeekController(presentationData: strongSelf.presentationData, content: content, sourceNode: { return sourceNode }) controller.visibilityUpdated = { [weak self] visible in diff --git a/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift index a91314ee4a..682002a352 100644 --- a/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift @@ -9,13 +9,14 @@ import SwiftSignalKit import StickerResources import AnimatedStickerNode import TelegramAnimatedStickerNode +import ContextUI public final class StickerPreviewPeekContent: PeekControllerContent { let account: Account public let item: ImportStickerPack.Sticker - let menu: [PeekControllerMenuItem] + let menu: [ContextMenuItem] - public init(account: Account, item: ImportStickerPack.Sticker, menu: [PeekControllerMenuItem]) { + public init(account: Account, item: ImportStickerPack.Sticker, menu: [ContextMenuItem]) { self.account = account self.item = item self.menu = menu @@ -25,11 +26,11 @@ public final class StickerPreviewPeekContent: PeekControllerContent { return .freeform } - public func menuActivation() -> PeerkControllerMenuActivation { + public func menuActivation() -> PeerControllerMenuActivation { return .press } - public func menuItems() -> [PeekControllerMenuItem] { + public func menuItems() -> [ContextMenuItem] { return self.menu } diff --git a/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift b/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift index 78ce35ae8e..ece1421f38 100644 --- a/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift +++ b/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift @@ -552,7 +552,12 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) let offset = self.contentContainerNode.frame.height - self.wrappingScrollNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + let position = self.wrappingScrollNode.position + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + self.wrappingScrollNode.position = CGPoint(x: position.x, y: position.y + offset) + transition.animateView({ + self.wrappingScrollNode.position = position + }) } func animateOut(completion: (() -> Void)? = nil) { diff --git a/submodules/PeerInfoUI/Sources/PeerReportController.swift b/submodules/PeerInfoUI/Sources/PeerReportController.swift index 146ce5ddbe..0b7d57f71f 100644 --- a/submodules/PeerInfoUI/Sources/PeerReportController.swift +++ b/submodules/PeerInfoUI/Sources/PeerReportController.swift @@ -33,7 +33,7 @@ public enum PeerReportOption { case other } -public func presentPeerReportOptions(context: AccountContext, parent: ViewController, contextController: ContextController?, backAction: ((ContextController) -> Void)? = nil, subject: PeerReportSubject, options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .other], passthrough: Bool = false, completion: @escaping (ReportReason?, Bool) -> Void) { +public func presentPeerReportOptions(context: AccountContext, parent: ViewController, contextController: ContextControllerProtocol?, backAction: ((ContextControllerProtocol) -> Void)? = nil, subject: PeerReportSubject, options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .other], passthrough: Bool = false, completion: @escaping (ReportReason?, Bool) -> Void) { if let contextController = contextController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var items: [ContextMenuItem] = [] @@ -163,7 +163,7 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro } contextController.setItems(.single(items)) } else { - contextController?.dismiss() + contextController?.dismiss(completion: nil) parent.view.endEditing(true) parent.present(peerReportOptionsController(context: context, subject: subject, passthrough: passthrough, present: { [weak parent] c, a in parent?.present(c, in: .window(.root), with: a) diff --git a/submodules/StickerPackPreviewUI/BUILD b/submodules/StickerPackPreviewUI/BUILD index bf34fcb5bb..d844048b4d 100644 --- a/submodules/StickerPackPreviewUI/BUILD +++ b/submodules/StickerPackPreviewUI/BUILD @@ -28,6 +28,7 @@ swift_library( "//submodules/ArchivedStickerPacksNotice:ArchivedStickerPacksNotice", "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/UndoUI:UndoUI", + "//submodules/ContextUI:ContextUI", ], visibility = [ "//visibility:public", diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift index 32f0b8cf59..6dcbd9e908 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift @@ -12,6 +12,7 @@ import MergeLists import ActivityIndicator import TextFormat import AccountContext +import ContextUI private struct StickerPackPreviewGridEntry: Comparable, Identifiable { let index: Int @@ -204,28 +205,26 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] if let stickerPack = strongSelf.stickerPack, case let .result(info, _, _) = stickerPack, info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { if strongSelf.sendSticker != nil { - menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, font: .bold, action: { node, rect in - if let strongSelf = self { - return strongSelf.sendSticker?(.standalone(media: item.file), node, rect) ?? false - } else { - return false - } - })) + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + +// let _ = strongSelf.sendSticker?(.standalone(media: item.file), node, rect) + }))) } - menuItems.append(PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in - if let strongSelf = self { - if isStarred { - let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() - } else { - let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() - } + menuItems.append(.action(ContextMenuActionItem(text: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + if let strongSelf = self { + if isStarred { + let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() + } else { + let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } - return true - })) - menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true })) + } + }))) } return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) } else { @@ -237,7 +236,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.presentationData.theme), content: content, sourceNode: { + let controller = PeekController(presentationData: strongSelf.presentationData, content: content, sourceNode: { return sourceNode }) controller.visibilityUpdated = { [weak self] visible in @@ -550,11 +549,17 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol func animateIn() { self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - + let offset: CGFloat = 510.0 let dimPosition = self.dimNode.layer.position - self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + let targetBounds = self.bounds + self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) + self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) + transition.animateView({ + self.bounds = targetBounds + self.dimNode.position = dimPosition + }) } func animateOut(completion: (() -> Void)? = nil) { diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 628ac687b3..ab05e621fb 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -11,6 +11,7 @@ import TelegramPresentationData import TelegramUIPreferences import MergeLists import ShimmerEffect +import ContextUI private struct StickerPackPreviewGridEntry: Comparable, Identifiable { let index: Int @@ -276,28 +277,26 @@ private final class StickerPackContainer: ASDisplayNode { |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] if let (info, _, _) = strongSelf.currentStickerPack, info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { if strongSelf.sendSticker != nil { - menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, font: .bold, action: { node, rect in - if let strongSelf = self { - return strongSelf.sendSticker?(.standalone(media: item.file), node, rect) ?? false - } else { - return false - } - })) + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + +// let _ = strongSelf.sendSticker?(.standalone(media: item.file), node, rect) + }))) } - menuItems.append(PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in - if let strongSelf = self { - if isStarred { - let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() - } else { - let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() - } + menuItems.append(.action(ContextMenuActionItem(text: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + if let strongSelf = self { + if isStarred { + let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() + } else { + let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } - return true - })) - menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true })) + } + }))) } return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) } else { @@ -309,7 +308,7 @@ private final class StickerPackContainer: ASDisplayNode { return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.presentationData.theme), content: content, sourceNode: { + let controller = PeekController(presentationData: strongSelf.presentationData, content: content, sourceNode: { return sourceNode }) strongSelf.presentInGlobalOverlay(controller, nil) diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift index 6af3b952f5..7f55f7cd67 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift @@ -9,6 +9,7 @@ import SwiftSignalKit import StickerResources import AnimatedStickerNode import TelegramAnimatedStickerNode +import ContextUI public enum StickerPreviewPeekItem: Equatable { case pack(StickerPackItem) @@ -27,9 +28,9 @@ public enum StickerPreviewPeekItem: Equatable { public final class StickerPreviewPeekContent: PeekControllerContent { let account: Account public let item: StickerPreviewPeekItem - let menu: [PeekControllerMenuItem] + let menu: [ContextMenuItem] - public init(account: Account, item: StickerPreviewPeekItem, menu: [PeekControllerMenuItem]) { + public init(account: Account, item: StickerPreviewPeekItem, menu: [ContextMenuItem]) { self.account = account self.item = item self.menu = menu @@ -39,11 +40,11 @@ public final class StickerPreviewPeekContent: PeekControllerContent { return .freeform } - public func menuActivation() -> PeerkControllerMenuActivation { + public func menuActivation() -> PeerControllerMenuActivation { return .press } - public func menuItems() -> [PeekControllerMenuItem] { + public func menuItems() -> [ContextMenuItem] { return self.menu } @@ -123,7 +124,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController let imageSize = dimensitons.cgSize.aspectFitted(boundingSize) self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() - let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: textSize.height + textSpacing), size: imageSize) + let imageFrame = CGRect(origin: CGPoint(x: 0.0, y: textSize.height + textSpacing), size: imageSize) self.imageNode.frame = imageFrame if let animationNode = self.animationNode { animationNode.frame = imageFrame @@ -132,7 +133,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController self.textNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - textSize.width) / 2.0), y: -textSize.height - textSpacing), size: textSize) - return CGSize(width: size.width, height: imageFrame.height + textSize.height + textSpacing) + return CGSize(width: imageFrame.width, height: imageFrame.height + textSize.height + textSpacing) } else { return CGSize(width: size.width, height: 10.0) } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift index bdcc420119..9a4c6d3923 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift @@ -68,6 +68,7 @@ final class VoiceChatCameraPreviewController: ViewController { } self.controllerNode.switchCamera = { [weak self] in self?.switchCamera() + self?.cameraNode.flip(withBackground: true) } self.controllerNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) @@ -287,15 +288,15 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) let targetBounds = self.bounds self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) + self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) transition.animateView({ self.bounds = targetBounds + self.dimNode.position = dimPosition }) self.applicationStateDisposable = (self.context.sharedContext.applicationBindings.applicationIsActive diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 3c538dade7..7f09d5c47f 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -43,7 +43,7 @@ private let topPanelHeight: CGFloat = 63.0 private let bottomAreaHeight: CGFloat = 205.0 private let fullscreenBottomAreaHeight: CGFloat = 80.0 -private func cornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? { +private func decorationCornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? { if !top && !bottom { return nil } @@ -73,6 +73,9 @@ final class GroupVideoNode: ASDisplayNode { private let videoViewContainer: UIView private let videoView: PresentationCallVideoView + private var effectView: UIVisualEffectView? + private var isBlurred: Bool = false + private var validLayout: (CGSize, Bool)? var tapped: (() -> Void)? @@ -121,6 +124,52 @@ final class GroupVideoNode: ASDisplayNode { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } + func updateIsBlurred(isBlurred: Bool, light: Bool = false, animated: Bool = true) { + if self.isBlurred == isBlurred { + return + } + self.isBlurred = isBlurred + + if isBlurred { + if self.effectView == nil { + let effectView = UIVisualEffectView() + self.effectView = effectView + effectView.frame = self.videoViewContainer.bounds + self.videoViewContainer.addSubview(effectView) + } + if animated { + UIView.animate(withDuration: 0.3, animations: { + self.effectView?.effect = UIBlurEffect(style: light ? .light : .dark) + }) + } else { + self.effectView?.effect = UIBlurEffect(style: light ? .light : .dark) + } + } else if let effectView = self.effectView { + self.effectView = nil + UIView.animate(withDuration: 0.3, animations: { + effectView.effect = nil + }, completion: { [weak effectView] _ in + effectView?.removeFromSuperview() + }) + } + } + + func flip(withBackground: Bool) { + if withBackground { + self.backgroundColor = .black + } + UIView.transition(with: withBackground ? self.videoViewContainer : self.view, duration: 0.4, options: [.transitionFlipFromLeft, .curveEaseOut], animations: { + UIView.performWithoutAnimation { + self.updateIsBlurred(isBlurred: true, light: true, animated: false) + } + }) { finished in + self.backgroundColor = nil + Queue.mainQueue().after(0.5) { + self.updateIsBlurred(isBlurred: false) + } + } + } + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.tapped?() @@ -129,7 +178,7 @@ final class GroupVideoNode: ASDisplayNode { func updateLayout(size: CGSize, isLandscape: Bool, transition: ContainedViewLayoutTransition) { self.validLayout = (size, isLandscape) - transition.updateFrame(view: self.videoViewContainer, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrameAsPositionAndBounds(layer: self.videoViewContainer.layer, frame: CGRect(origin: CGPoint(), size: size)) let orientation = self.videoView.getOrientation() var aspect = self.videoView.getAspect() @@ -160,32 +209,39 @@ final class GroupVideoNode: ASDisplayNode { } var rotatedVideoSize = CGSize(width: 100.0, height: rotatedAspect * 100.0) - - if isLandscape { - rotatedVideoSize = rotatedVideoSize.aspectFitted(size) - } else { - rotatedVideoSize = rotatedVideoSize.aspectFilled(size) - } - + + var containerSize = size if switchOrientation { rotatedVideoSize = CGSize(width: rotatedVideoSize.height, height: rotatedVideoSize.width) + containerSize = CGSize(width: containerSize.height, height: containerSize.width) } + + if isLandscape { + rotatedVideoSize = rotatedVideoSize.aspectFitted(containerSize) + } else { + rotatedVideoSize = rotatedVideoSize.aspectFilled(containerSize) + } + var rotatedVideoFrame = CGRect(origin: CGPoint(x: floor((size.width - rotatedVideoSize.width) / 2.0), y: floor((size.height - rotatedVideoSize.height) / 2.0)), size: rotatedVideoSize) rotatedVideoFrame.origin.x = floor(rotatedVideoFrame.origin.x) rotatedVideoFrame.origin.y = floor(rotatedVideoFrame.origin.y) rotatedVideoFrame.size.width = ceil(rotatedVideoFrame.size.width) rotatedVideoFrame.size.height = ceil(rotatedVideoFrame.size.height) - var videoSize = rotatedVideoFrame.size + let videoSize = rotatedVideoFrame.size.aspectFilled(CGSize(width: 1080.0, height: 1080.0)) transition.updatePosition(layer: self.videoView.view.layer, position: rotatedVideoFrame.center) transition.updateBounds(layer: self.videoView.view.layer, bounds: CGRect(origin: CGPoint(), size: videoSize)) - let scale = rotatedVideoFrame.width / videoSize.width - transition.updateTransformScale(layer: self.videoView.view.layer, scale: scale) + let transformScale: CGFloat = rotatedVideoFrame.width / videoSize.width + transition.updateTransformScale(layer: self.videoViewContainer.layer, scale: transformScale) let transition: ContainedViewLayoutTransition = .immediate transition.updateTransformRotation(view: self.videoView.view, angle: angle) + if let effectView = self.effectView { + transition.updateFrame(view: effectView, frame: self.videoViewContainer.bounds) + } + // TODO: properly fix the issue // On iOS 13 and later metal layer transformation is broken if the layer does not require compositing self.videoView.view.alpha = 0.995 @@ -201,8 +257,8 @@ private final class MainVideoContainerNode: ASDisplayNode { private var currentVideoNode: GroupVideoNode? private var candidateVideoNode: GroupVideoNode? - fileprivate let otherVideoButtonNode: HighlightTrackingButtonNode - private let otherVideoWrapperNode: ASDisplayNode + private let otherVideoButtonNode: HighlightTrackingButtonNode + fileprivate let otherVideoWrapperNode: ASDisplayNode private let otherVideoShadowNode: ASImageNode private var otherVideoNode: GroupVideoNode? @@ -217,17 +273,20 @@ private final class MainVideoContainerNode: ASDisplayNode { var tapped: (() -> Void)? var otherVideoTapped: (() -> Void)? + private let videoReadyDisposable = MetaDisposable() + private let otherVideoReadyDisposable = MetaDisposable() + init(context: AccountContext, call: PresentationGroupCall) { self.context = context self.call = call self.topCornersNode = ASImageNode() self.topCornersNode.displaysAsynchronously = false - self.topCornersNode.image = cornersImage(top: true, bottom: false, dark: true) + self.topCornersNode.image = decorationCornersImage(top: true, bottom: false, dark: true) self.bottomCornersNode = ASImageNode() self.bottomCornersNode.displaysAsynchronously = false - self.bottomCornersNode.image = cornersImage(top: false, bottom: true, dark: true) + self.bottomCornersNode.image = decorationCornersImage(top: false, bottom: true, dark: true) self.bottomEdgeNode = ASDisplayNode() self.bottomEdgeNode.backgroundColor = UIColor(rgb: 0x000000) @@ -264,6 +323,8 @@ private final class MainVideoContainerNode: ASDisplayNode { self.otherVideoButtonNode.cornerRadius = 5.5 self.otherVideoWrapperNode = ASDisplayNode() + self.otherVideoWrapperNode.alpha = 0.0 + self.otherVideoWrapperNode.transform = CATransform3DMakeScale(0.001, 0.001, 1.0) super.init() @@ -282,6 +343,11 @@ private final class MainVideoContainerNode: ASDisplayNode { self.otherVideoButtonNode.addTarget(self, action: #selector(self.otherVideoPressed), forControlEvents: .touchUpInside) } + deinit { + self.videoReadyDisposable.dispose() + self.otherVideoReadyDisposable.dispose() + } + override func didLoad() { super.didLoad() @@ -296,13 +362,14 @@ private final class MainVideoContainerNode: ASDisplayNode { self.otherVideoTapped?() } - func updatePeer(peer: (peerId: PeerId, endpointId: String, otherEndpointId: String?)?, waitForFullSize: Bool) { + func updatePeer(peer: (peerId: PeerId, endpointId: String, otherEndpointId: String?)?, waitForFullSize: Bool, completion: (() -> Void)? = nil) { if self.currentPeer?.0 == peer?.0 && self.currentPeer?.1 == peer?.1 && self.currentPeer?.2 == peer?.2 { return } let previousPeer = self.currentPeer self.currentPeer = peer if let (_, endpointId, otherEndpointId) = peer { + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) if let otherEndpointId = otherEndpointId { if otherEndpointId != previousPeer?.2 { self.call.makeIncomingVideoView(endpointId: otherEndpointId) { [weak self] videoView in @@ -320,12 +387,35 @@ private final class MainVideoContainerNode: ASDisplayNode { if let (size, sideInset, isLandscape) = strongSelf.validLayout { strongSelf.update(size: size, sideInset: sideInset, isLandscape: isLandscape, transition: .immediate) } + + if strongSelf.otherVideoWrapperNode.alpha.isZero { + strongSelf.otherVideoReadyDisposable.set((videoNode.ready + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + if let strongSelf = self { + transition.updateAlpha(node: strongSelf.otherVideoWrapperNode, alpha: 1.0) + transition.updateTransformScale(node: strongSelf.otherVideoWrapperNode, scale: 1.0) + } + })) + } } } } else { if let otherVideoNode = self.otherVideoNode { - otherVideoNode.removeFromSupernode() self.otherVideoNode = nil + self.otherVideoReadyDisposable.set(nil) + let completion = { + otherVideoNode.removeFromSupernode() + } + if !self.otherVideoWrapperNode.alpha.isZero { + transition.updateAlpha(node: self.otherVideoWrapperNode, alpha: 0.0, completion: { finished in + completion() + }) + transition.updateTransformScale(node: self.otherVideoWrapperNode, scale: 0.001) + } else { + completion() + } } } if endpointId != previousPeer?.1 { @@ -334,44 +424,38 @@ private final class MainVideoContainerNode: ASDisplayNode { guard let strongSelf = self, let videoView = videoView else { return } - - if waitForFullSize { - let candidateVideoNode = GroupVideoNode(videoView: videoView) - strongSelf.candidateVideoNode = candidateVideoNode - - Queue.mainQueue().after(0.3, { [weak candidateVideoNode] in - guard let strongSelf = self, let videoNode = candidateVideoNode, videoNode === strongSelf.candidateVideoNode else { - return - } - - if let currentVideoNode = strongSelf.currentVideoNode { - currentVideoNode.removeFromSupernode() - strongSelf.currentVideoNode = nil - } - strongSelf.currentVideoNode = videoNode - strongSelf.insertSubnode(videoNode, belowSubnode: strongSelf.topCornersNode) - if let (size, sideInset, isLandscape) = strongSelf.validLayout { - strongSelf.update(size: size, sideInset: sideInset, isLandscape: isLandscape, transition: .immediate) - } + + let videoNode = GroupVideoNode(videoView: videoView) + if let currentVideoNode = strongSelf.currentVideoNode { + strongSelf.currentVideoNode = nil + + currentVideoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak currentVideoNode] _ in + currentVideoNode?.removeFromSupernode() }) + } + strongSelf.currentVideoNode = videoNode + strongSelf.insertSubnode(videoNode, belowSubnode: strongSelf.topCornersNode) + if let (size, sideInset, isLandscape) = strongSelf.validLayout { + strongSelf.update(size: size, sideInset: sideInset, isLandscape: isLandscape, transition: .immediate) + } + + if waitForFullSize { + strongSelf.videoReadyDisposable.set((videoNode.ready + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { _ in + completion?() + })) } else { - strongSelf.candidateVideoNode = nil - - let videoNode = GroupVideoNode(videoView: videoView) - if let currentVideoNode = strongSelf.currentVideoNode { - currentVideoNode.removeFromSupernode() - strongSelf.currentVideoNode = nil - } - strongSelf.currentVideoNode = videoNode - strongSelf.insertSubnode(videoNode, belowSubnode: strongSelf.topCornersNode) - if let (size, sideInset, isLandscape) = strongSelf.validLayout { - strongSelf.update(size: size, sideInset: sideInset, isLandscape: isLandscape, transition: .immediate) - } + strongSelf.videoReadyDisposable.set(nil) + completion?() } } }) } } else { + self.videoReadyDisposable.set(nil) + self.otherVideoReadyDisposable.set(nil) if let currentVideoNode = self.currentVideoNode { currentVideoNode.removeFromSupernode() self.currentVideoNode = nil @@ -384,7 +468,7 @@ private final class MainVideoContainerNode: ASDisplayNode { if let currentVideoNode = self.currentVideoNode { transition.updateFrame(node: currentVideoNode, frame: CGRect(origin: CGPoint(), size: size)) - currentVideoNode.updateLayout(size: size, isLandscape: isLandscape, transition: transition) + currentVideoNode.updateLayout(size: size, isLandscape: true, transition: transition) } let smallVideoSize = CGSize(width: 40.0, height: 40.0) @@ -430,6 +514,7 @@ public final class VoiceChatController: ViewController { private final class Interaction { let updateIsMuted: (PeerId, Bool) -> Void let pinPeer: (PeerId) -> Void + let togglePeerVideo: (PeerId) -> Void let openInvite: () -> Void let peerContextAction: (PeerEntry, ASDisplayNode, ContextGesture?) -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void @@ -443,6 +528,7 @@ public final class VoiceChatController: ViewController { init( updateIsMuted: @escaping (PeerId, Bool) -> Void, pinPeer: @escaping (PeerId) -> Void, + togglePeerVideo: @escaping (PeerId) -> Void, openInvite: @escaping () -> Void, peerContextAction: @escaping (PeerEntry, ASDisplayNode, ContextGesture?) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, @@ -450,6 +536,7 @@ public final class VoiceChatController: ViewController { ) { self.updateIsMuted = updateIsMuted self.pinPeer = pinPeer + self.togglePeerVideo = togglePeerVideo self.openInvite = openInvite self.peerContextAction = peerContextAction self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions @@ -797,13 +884,12 @@ public final class VoiceChatController: ViewController { }, action: { node in if case .list = peerEntry.style { interaction.peerContextAction(peerEntry, node, nil) - } else { - interaction.pinPeer(peer.id) -// if peerEntry.pinned { -// interaction.peerContextAction(peerEntry, node, nil) -// } else if let endpointId = peerEntry.effectiveVideoEndpointId { -// interaction.pinPeer(peer.id) -// } + } else if peerEntry.effectiveVideoEndpointId != nil { + if peerEntry.pinned && peerEntry.videoEndpointId != nil && peerEntry.screencastEndpointId != nil { + interaction.togglePeerVideo(peer.id) + } else { + interaction.pinPeer(peer.id) + } } }, contextAction: peerEntry.style == .list ? { node, gesture in interaction.peerContextAction(peerEntry, node, gesture) @@ -1005,10 +1091,9 @@ public final class VoiceChatController: ViewController { self.mainVideoClippingNode = ASDisplayNode() self.mainVideoClippingNode.clipsToBounds = true + + self.mainVideoContainerNode = MainVideoContainerNode(context: call.accountContext, call: call) - if sharedContext.immediateExperimentalUISettings.demoVideoChats { - self.mainVideoContainerNode = MainVideoContainerNode(context: call.accountContext, call: call) - } self.mainParticipantNode = VoiceChatParticipantItemNode() self.toggleFullscreenButton = HighlightTrackingButtonNode() @@ -1057,7 +1142,7 @@ public final class VoiceChatController: ViewController { self.topCornersNode = ASImageNode() self.topCornersNode.displaysAsynchronously = false self.topCornersNode.displayWithoutProcessing = true - self.topCornersNode.image = cornersImage(top: true, bottom: false, dark: false) + self.topCornersNode.image = decorationCornersImage(top: true, bottom: false, dark: false) self.bottomPanelCoverNode = ASDisplayNode() self.bottomPanelCoverNode.backgroundColor = fullscreenBackgroundColor @@ -1072,7 +1157,7 @@ public final class VoiceChatController: ViewController { self.bottomCornersNode = ASImageNode() self.bottomCornersNode.displaysAsynchronously = false self.bottomCornersNode.displayWithoutProcessing = true - self.bottomCornersNode.image = cornersImage(top: false, bottom: true, dark: false) + self.bottomCornersNode.image = decorationCornersImage(top: false, bottom: true, dark: false) self.bottomCornersNode.isUserInteractionEnabled = false self.audioButton = CallControllerButtonItemNode() @@ -1168,8 +1253,13 @@ public final class VoiceChatController: ViewController { } else { strongSelf.currentForcedSpeakerWithVideo = nil } - strongSelf.updatePinnedParticipant(waitForFullSize: false) + strongSelf.updateMainParticipant(waitForFullSize: false) } + }, togglePeerVideo: { [weak self] peerId in + guard let strongSelf = self else { + return + } + strongSelf.mainVideoContainerNode?.otherVideoTapped?() }, openInvite: { [weak self] in guard let strongSelf = self else { return @@ -1973,10 +2063,9 @@ public final class VoiceChatController: ViewController { } if let (peerId, _) = maxLevelWithVideo { - if strongSelf.currentDominantSpeakerWithVideo != peerId { - strongSelf.currentDominantSpeakerWithVideo = peerId - - strongSelf.updatePinnedParticipant(waitForFullSize: true) + strongSelf.currentDominantSpeakerWithVideo = peerId + if !strongSelf.requestedVideoSources.isEmpty { + strongSelf.updateMainParticipant(waitForFullSize: false) } } @@ -2076,9 +2165,7 @@ public final class VoiceChatController: ViewController { let videoNode = GroupVideoNode(videoView: videoView) strongSelf.videoNodes.append((endpointId, videoNode)) - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) - + if let _ = strongSelf.validLayout { loop: for i in 0 ..< strongSelf.currentEntries.count { let entry = strongSelf.currentEntries[i] let tileEntry = strongSelf.currentTileEntries[i] @@ -2110,7 +2197,6 @@ public final class VoiceChatController: ViewController { strongSelf.requestedVideoSources.remove(source) } - var updated = false for i in (0 ..< strongSelf.videoNodes.count).reversed() { if !validSources.contains(strongSelf.videoNodes[i].0) { let endpointId = strongSelf.videoNodes[i].0 @@ -2123,15 +2209,14 @@ public final class VoiceChatController: ViewController { case let .peer(peerEntry): if peerEntry.effectiveVideoEndpointId == endpointId { let presentationData = strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme) - strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.item(context: strongSelf.context, presentationData: presentationData, interaction: strongSelf.itemInteraction!, transparent: false), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil) - strongSelf.tileListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: tileEntry.item(context: strongSelf.context, presentationData: presentationData, interaction: strongSelf.itemInteraction!, transparent: false), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil) + strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: j, previousIndex: j, item: entry.item(context: strongSelf.context, presentationData: presentationData, interaction: strongSelf.itemInteraction!, transparent: false), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil) + strongSelf.tileListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: j, previousIndex: j, item: tileEntry.item(context: strongSelf.context, presentationData: presentationData, interaction: strongSelf.itemInteraction!, transparent: false), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil) break loop } default: break } } - updated = true } } @@ -2143,13 +2228,12 @@ public final class VoiceChatController: ViewController { if peerId == strongSelf.currentDominantSpeakerWithVideo { strongSelf.currentDominantSpeakerWithVideo = nil } - strongSelf.updatePinnedParticipant(waitForFullSize: false) + strongSelf.updateMainParticipant(waitForFullSize: false) } - } - - if updated { - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) + } else if strongSelf.currentDominantSpeakerWithVideo != nil && !strongSelf.requestedVideoSources.isEmpty && !strongSelf.didSetMainParticipant { + strongSelf.didSetMainParticipant = true + Queue.mainQueue().after(0.1) { + strongSelf.updateMainParticipant(waitForFullSize: true) } } })) @@ -2177,7 +2261,7 @@ public final class VoiceChatController: ViewController { } self.mainVideoContainerNode?.tapped = { [weak self] in - if let strongSelf = self { + if let strongSelf = self, !strongSelf.animatingExpansion { var effectiveDisplayMode = strongSelf.displayMode var isLandscape = false if let (layout, _) = strongSelf.validLayout, layout.size.width > layout.size.height, case .compact = layout.metrics.widthClass { @@ -2322,7 +2406,7 @@ public final class VoiceChatController: ViewController { } else { strongSelf.switchedToCameraPeers.remove(peerId) } - strongSelf.updatePinnedParticipant(waitForFullSize: false, force: true) + strongSelf.updateMainParticipant(waitForFullSize: false, force: true) strongSelf.displayToggleVideoSourceTooltip(screencast: !switchingToCamera) } } @@ -3332,10 +3416,16 @@ public final class VoiceChatController: ViewController { } } + private var animatingButtons = false @objc private func cameraPressed() { if self.call.hasVideo || self.call.hasScreencast { self.call.disableVideo() self.call.disableScreencast() + + if let (layout, navigationHeight) = self.validLayout { + self.animatingButtons = true + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .linear)) + } } else { self.call.makeOutgoingVideoView { [weak self] view in guard let strongSelf = self, let view = view else { @@ -3345,6 +3435,11 @@ public final class VoiceChatController: ViewController { let controller = VoiceChatCameraPreviewController(context: strongSelf.context, cameraNode: cameraNode, shareCamera: { [weak self] videoNode in if let strongSelf = self { strongSelf.call.requestVideo() + + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.animatingButtons = true + strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .linear)) + } } }, switchCamera: { [weak self] in self?.call.switchVideoCamera() @@ -3374,6 +3469,10 @@ public final class VoiceChatController: ViewController { return controlsHidden ? 0.0 : fullscreenBottomAreaHeight } } + + private var hasMainVideo: Bool { + return self.mainVideoContainerNode != nil && self.effectiveSpeakerWithVideo != nil + } private var bringVideoToBackOnCompletion = false private func updateDecorationsLayout(transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) { @@ -3411,7 +3510,7 @@ public final class VoiceChatController: ViewController { let listSize = CGSize(width: contentWidth, height: layout.size.height - listTopInset - bottomPanelHeight) let topInset: CGFloat if let (panInitialTopInset, panOffset) = self.panGestureArguments { - if self.isExpanded { + if self.isExpanded && !self.hasMainVideo { topInset = min(self.topInset ?? listSize.height, panInitialTopInset + max(0.0, panOffset)) } else { topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) @@ -3473,7 +3572,7 @@ public final class VoiceChatController: ViewController { videoY = layout.statusBarHeight ?? 20.0 isFullscreen = true } - videoClippingFrame = CGRect(origin: CGPoint(x: videoInset, y: videoY), size: CGSize(width: layout.size.width - videoInset * 2.0, height: self.isFullscreen ? videoHeight : 0.0)) + videoClippingFrame = CGRect(origin: CGPoint(x: videoInset, y: videoY), size: CGSize(width: layout.size.width - videoInset * 2.0, height: self.hasMainVideo ? videoHeight : 0.0)) videoContainerFrame = CGRect(origin: CGPoint(x: -videoInset, y: 0.0), size: CGSize(width: layout.size.width, height: videoHeight)) } @@ -3578,7 +3677,7 @@ public final class VoiceChatController: ViewController { let previousBottomCornersFrame = self.bottomCornersNode.frame if !bottomCornersFrame.equalTo(previousBottomCornersFrame) { self.bottomCornersNode.frame = bottomCornersFrame - self.bottomPanelBackgroundNode.frame = CGRect(x: 0.0, y: bottomOffset, width: size.width, height: 2000.0) + self.bottomPanelBackgroundNode.frame = CGRect(x: 0.0, y: bottomOffset + bottomDelta, width: size.width, height: 2000.0) let positionDelta = CGPoint(x: 0.0, y: previousBottomCornersFrame.minY - bottomCornersFrame.minY) transition.animatePositionAdditive(node: self.bottomCornersNode, offset: positionDelta) @@ -3654,7 +3753,7 @@ public final class VoiceChatController: ViewController { snapshotView?.removeFromSuperview() }) } - self.topCornersNode.image = cornersImage(top: true, bottom: false, dark: isFullscreen) + self.topCornersNode.image = decorationCornersImage(top: true, bottom: false, dark: isFullscreen) if let snapshotView = self.bottomCornersNode.view.snapshotContentTree() { snapshotView.frame = self.bottomCornersNode.bounds @@ -3664,7 +3763,7 @@ public final class VoiceChatController: ViewController { snapshotView?.removeFromSuperview() }) } - self.bottomCornersNode.image = cornersImage(top: false, bottom: true, dark: isFullscreen) + self.bottomCornersNode.image = decorationCornersImage(top: false, bottom: true, dark: isFullscreen) if !self.optionsButtonIsAvatar { self.optionsButton.setContent(.more(optionsCircleImage(dark: isFullscreen)), animated: transition.isAnimated) @@ -3803,8 +3902,14 @@ public final class VoiceChatController: ViewController { let hasVideo = self.call.hasVideo || self.call.hasScreencast let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .linear) : .immediate - self.cameraButton.update(size: videoButtonSize, content: CallControllerButtonItemNode.Content(appearance: normalButtonAppearance, image: hasVideo ? .cameraOn : .cameraOff), text: self.presentationData.strings.VoiceChat_Video, transition: transition) + self.cameraButton.update(size: hasVideo ? sideButtonSize : videoButtonSize, content: CallControllerButtonItemNode.Content(appearance: hasVideo ? activeButtonAppearance : normalButtonAppearance, image: hasVideo ? .cameraOn : .cameraOff), text: self.presentationData.strings.VoiceChat_Video, transition: transition) + transition.updateAlpha(node: self.switchCameraButton, alpha: hasVideo ? 1.0 : 0.0) + transition.updateTransformScale(node: self.switchCameraButton, scale: hasVideo ? 1.0 : 0.0) + + transition.updateAlpha(node: self.audioButton, alpha: hasVideo ? 0.0 : 1.0) + transition.updateTransformScale(node: self.audioButton, scale: hasVideo ? 0.0 : 1.0) + self.switchCameraButton.update(size: videoButtonSize, content: CallControllerButtonItemNode.Content(appearance: normalButtonAppearance, image: .flipCamera), text: "", transition: transition) self.audioButton.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage, isEnabled: soundEnabled), text: soundTitle, transition: transition) @@ -3812,7 +3917,7 @@ public final class VoiceChatController: ViewController { self.leaveButton.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.custom(0xff3b30, 0.3)), image: .cancel), text: self.presentationData.strings.VoiceChat_Leave, transition: .immediate) - transition.updateAlpha(node: self.cameraButton.textNode, alpha: 0.0) + transition.updateAlpha(node: self.cameraButton.textNode, alpha: hasVideo ? buttonsTitleAlpha : 0.0) transition.updateAlpha(node: self.switchCameraButton.textNode, alpha: buttonsTitleAlpha) transition.updateAlpha(node: self.audioButton.textNode, alpha: buttonsTitleAlpha) transition.updateAlpha(node: self.leaveButton.textNode, alpha: buttonsTitleAlpha) @@ -3847,7 +3952,9 @@ public final class VoiceChatController: ViewController { if !self.isFullscreen { self.isExpanded = true self.updateIsFullscreen(true) -// self.tileListNode.isHidden = false + } + if self.hasMainVideo { + self.tileListNode.isHidden = false } if case .fullscreen = effectiveDisplayMode { } else { @@ -3913,8 +4020,7 @@ public final class VoiceChatController: ViewController { var topCornersY = topPanelHeight if isLandscape { listTopInset = topPanelHeight -// topCornersY = -50.0 - } else if self.mainVideoContainerNode != nil && self.isFullscreen { + } else if self.hasMainVideo && self.isExpanded { let videoContainerHeight = min(mainVideoHeight, layout.size.width) listTopInset += videoContainerHeight topCornersY += videoContainerHeight @@ -3923,7 +4029,7 @@ public final class VoiceChatController: ViewController { let listSize = CGSize(width: contentWidth, height: layout.size.height - listTopInset - (isLandscape ? layout.intrinsicInsets.bottom : bottomPanelHeight)) let topInset: CGFloat if let (panInitialTopInset, panOffset) = self.panGestureArguments { - if self.isExpanded { + if self.isExpanded && !self.hasMainVideo { topInset = min(self.topInset ?? listSize.height, panInitialTopInset + max(0.0, panOffset)) } else { topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) @@ -4163,10 +4269,21 @@ public final class VoiceChatController: ViewController { self.updateButtons(animated: !isFirstTime) if self.audioButton.supernode === self.bottomPanelNode { - transition.updateFrame(node: self.cameraButton, frame: firstButtonFrame) - transition.updateFrame(node: self.switchCameraButton, frame: firstButtonFrame) - transition.updateFrame(node: self.audioButton, frame: secondButtonFrame) - transition.updateFrame(node: self.leaveButton, frame: forthButtonFrame) + transition.updateFrameAsPositionAndBounds(node: self.switchCameraButton, frame: firstButtonFrame) + + if !self.animatingButtons || transition.isAnimated { + if self.call.hasVideo { + transition.updateFrameAsPositionAndBounds(node: self.cameraButton, frame: secondButtonFrame, completion: { [weak self] _ in + self?.animatingButtons = false + }) + } else { + transition.updateFrameAsPositionAndBounds(node: self.cameraButton, frame: firstButtonFrame, completion: { [weak self] _ in + self?.animatingButtons = false + }) + } + } + transition.updateFrameAsPositionAndBounds(node: self.audioButton, frame: secondButtonFrame) + transition.updateFrameAsPositionAndBounds(node: self.leaveButton, frame: forthButtonFrame) } if isFirstTime { while !self.enqueuedTransitions.isEmpty { @@ -4182,6 +4299,11 @@ public final class VoiceChatController: ViewController { guard let (layout, navigationHeight) = self.validLayout else { return } + + if self.hasMainVideo && !self.isFullscreen { + self.updateIsFullscreen(true) + } + self.updateDecorationsLayout(transition: .immediate) self.animatingAppearance = true @@ -4376,7 +4498,7 @@ public final class VoiceChatController: ViewController { }) } - private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, callMembers: ([GroupCallParticipantsContext.Participant], String?), invitedPeers: [Peer], speakingPeers: Set) { + private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, callMembers: ([GroupCallParticipantsContext.Participant], String?), invitedPeers: [Peer], speakingPeers: Set, updatePinnedPeer: Bool = true) { var disableAnimation = false if self.currentCallMembers?.1 != callMembers.1 { disableAnimation = true @@ -4532,7 +4654,7 @@ public final class VoiceChatController: ViewController { volume: member.volume, raisedHand: member.hasRaiseHand, displayRaisedHandStatus: self.displayedRaisedHands.contains(member.peer.id), - pinned: true, + pinned: memberPeer.id == self.currentForcedSpeakerWithVideo, style: .list )) } @@ -4572,8 +4694,27 @@ public final class VoiceChatController: ViewController { self.endpointToPeerId = endpointIdToPeerId self.peerIdToEndpoint = peerIdToEndpointId + let previousPinnedEntry = self.pinnedEntry self.pinnedEntry = pinnedEntry + var previousPinnedPeerEntry: PeerEntry? + var pinnedPeerEntry: PeerEntry? + + if let previousPinnedEntry = previousPinnedEntry, case let .peer(previousPeerEntry) = previousPinnedEntry { + previousPinnedPeerEntry = previousPeerEntry + } + if let pinnedEntry = pinnedEntry, case let .peer(peerEntry) = pinnedEntry { + pinnedPeerEntry = peerEntry + } + if previousPinnedPeerEntry?.peer.id != pinnedPeerEntry?.peer.id { + self.updateDecorationsLayout(transition: .animated(duration: 0.2, curve: .easeInOut)) + } + + if updatePinnedPeer && (previousPinnedPeerEntry?.videoEndpointId != pinnedPeerEntry?.videoEndpointId || previousPinnedPeerEntry?.screencastEndpointId != pinnedPeerEntry?.screencastEndpointId) { + self.updateMainParticipant(waitForFullSize: false, currentEntries: entries, updateMembers: true, force: true) + return + } + let previousEntries = self.currentEntries let previousTileEntries = self.currentTileEntries self.currentEntries = entries @@ -4612,20 +4753,24 @@ public final class VoiceChatController: ViewController { self.enqueueTileTransition(tileTransition) } - private func updatePinnedParticipant(waitForFullSize: Bool, force: Bool = false) { - let effectivePinnedParticipant = self.currentForcedSpeakerWithVideo ?? self.currentDominantSpeakerWithVideo - guard effectivePinnedParticipant != self.effectiveSpeakerWithVideo?.0 || force else { + private var didSetMainParticipant = false + private func updateMainParticipant(waitForFullSize: Bool, currentEntries: [ListEntry]? = nil, updateMembers: Bool = true, force: Bool = false) { + let effectiveMainParticipant = self.currentForcedSpeakerWithVideo ?? self.currentDominantSpeakerWithVideo + guard effectiveMainParticipant != self.effectiveSpeakerWithVideo?.0 || force else { return } - var hasVideo = false - if let peerId = effectivePinnedParticipant { - for entry in self.currentEntries { + let currentEntries = currentEntries ?? self.currentEntries + + var effectivePeer: (PeerId, String, String?)? = nil + var anyPeer: (PeerId, String, String?)? = nil + if let peerId = effectiveMainParticipant { + for entry in currentEntries { switch entry { case let .peer(peer): if peer.peer.id == peerId { var effectiveEndpointId = peer.effectiveVideoEndpointId - if self.switchedToCameraPeers.contains(peerId), let videoEndpointId = peer.videoEndpointId { + if self.switchedToCameraPeers.contains(peer.peer.id), let videoEndpointId = peer.videoEndpointId { effectiveEndpointId = videoEndpointId } @@ -4637,10 +4782,23 @@ public final class VoiceChatController: ViewController { } if let endpointId = effectiveEndpointId { - hasVideo = true - self.effectiveSpeakerWithVideo = (peerId, endpointId) - self.call.setFullSizeVideo(endpointId: endpointId) - self.mainVideoContainerNode?.updatePeer(peer: (peerId: peerId, endpointId: endpointId, otherEndpointId: otherEndpointId), waitForFullSize: waitForFullSize) + effectivePeer = (peer.peer.id, endpointId, otherEndpointId) + } + } else if anyPeer == nil && peer.effectiveVideoEndpointId != nil { + var effectiveEndpointId = peer.effectiveVideoEndpointId + if self.switchedToCameraPeers.contains(peer.peer.id), let videoEndpointId = peer.videoEndpointId { + effectiveEndpointId = videoEndpointId + } + + var otherEndpointId: String? + if effectiveEndpointId != peer.videoEndpointId { + otherEndpointId = peer.videoEndpointId + } else if effectiveEndpointId != peer.screencastEndpointId { + otherEndpointId = peer.screencastEndpointId + } + + if let endpointId = effectiveEndpointId { + anyPeer = (peer.peer.id, endpointId, otherEndpointId) } } default: @@ -4648,33 +4806,57 @@ public final class VoiceChatController: ViewController { } } } - - if !hasVideo { - self.effectiveSpeakerWithVideo = nil - self.call.setFullSizeVideo(endpointId: nil) - self.mainVideoContainerNode?.updatePeer(peer: nil, waitForFullSize: false) + + if effectivePeer == nil { + effectivePeer = anyPeer } - self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set()) - - var updateLayout = false - if self.effectiveSpeakerWithVideo != nil && !self.isExpanded { - self.isExpanded = true - updateLayout = true - } else if self.effectiveSpeakerWithVideo == nil && self.isExpanded { - self.isExpanded = false - updateLayout = true - } - - if updateLayout { - self.updateIsFullscreen(self.isExpanded) - self.animatingExpansion = true - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + let completion = { + var updateLayout = false + if self.effectiveSpeakerWithVideo != nil && !self.isExpanded { + self.isExpanded = true + updateLayout = true + } else if self.effectiveSpeakerWithVideo == nil && self.isExpanded { + self.isExpanded = false + updateLayout = true } - self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: { - self.animatingExpansion = false - }) + + if updateLayout { + self.updateIsFullscreen(self.isExpanded) + self.animatingExpansion = true + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring) + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) + } + self.updateDecorationsLayout(transition: transition, completion: { + self.animatingExpansion = false + }) + } + } + + var waitForFullSize = waitForFullSize + if !self.isExpanded { + waitForFullSize = true + self.mainVideoClippingNode.alpha = 0.0 + } + + self.effectiveSpeakerWithVideo = effectivePeer.flatMap { ($0.0, $0.1) } + if updateMembers { + self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set(), updatePinnedPeer: false) + } + self.call.setFullSizeVideo(endpointId: effectivePeer?.1) + self.mainVideoContainerNode?.updatePeer(peer: effectivePeer, waitForFullSize: waitForFullSize, completion: { [weak self] in + if waitForFullSize { + completion() + + if let strongSelf = self, strongSelf.mainVideoClippingNode.alpha.isZero { + strongSelf.mainVideoClippingNode.alpha = 1.0 + strongSelf.mainVideoClippingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + } + }) + if !waitForFullSize { + completion() } } @@ -4732,6 +4914,7 @@ public final class VoiceChatController: ViewController { if isScheduling && translation < 0.0 { return } + var topInset: CGFloat = 0.0 if let (currentTopInset, currentPanOffset) = self.panGestureArguments { topInset = currentTopInset @@ -4754,7 +4937,7 @@ public final class VoiceChatController: ViewController { self.updateIsFullscreen(false) } - if self.isExpanded { + if self.isExpanded && !self.hasMainVideo { } else { if currentOffset > 0.0 { self.listNode.scroller.panGestureRecognizer.setTranslation(CGPoint(), in: self.listNode.scroller) @@ -4766,7 +4949,7 @@ public final class VoiceChatController: ViewController { self.updateDecorationsLayout(transition: .immediate) } - if !self.isExpanded { + if !self.isExpanded || self.hasMainVideo { var bounds = self.contentContainer.bounds bounds.origin.y = -translation bounds.origin.y = min(0.0, bounds.origin.y) @@ -4800,7 +4983,7 @@ public final class VoiceChatController: ViewController { topInset = self.listNode.frame.height } - if self.isExpanded { + if self.isExpanded && !self.hasMainVideo { self.panGestureArguments = nil if velocity.y > 300.0 || offset > topInset / 2.0 { self.isExpanded = false @@ -4852,7 +5035,7 @@ public final class VoiceChatController: ViewController { self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: { self.animatingExpansion = false }) - } else if !isScheduling { + } else if !isScheduling && !self.hasMainVideo { self.updateIsFullscreen(false) self.animatingExpansion = true self.listNode.scroller.setContentOffset(CGPoint(), animated: false) @@ -4864,7 +5047,7 @@ public final class VoiceChatController: ViewController { self.animatingExpansion = false }) } - if !dismissing { + if !dismissing && self.hasMainVideo { var bounds = self.contentContainer.bounds let previousBounds = bounds bounds.origin.y = 0.0 @@ -5222,7 +5405,7 @@ public final class VoiceChatController: ViewController { switch self.displayMode { case .default: - let location = videoContainerNode.view.convert(videoContainerNode.otherVideoButtonNode.frame, to: nil) + let location = videoContainerNode.view.convert(videoContainerNode.otherVideoWrapperNode.frame, to: nil) self.controller?.present(TooltipScreen(text: screencast ? self.presentationData.strings.VoiceChat_TapToViewCameraVideo : self.presentationData.strings.VoiceChat_TapToViewScreenVideo, icon: nil, location: .point(location.offsetBy(dx: -9.0, dy: 0.0), .right), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in return .dismiss(consume: false) }), in: .window(.root)) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift index e9af74911a..341328fba7 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift @@ -17,7 +17,7 @@ public final class VoiceChatInfoContextItem: ContextMenuCustomItem { self.icon = icon } - public func node(presentationData: PresentationData, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { return VoiceChatInfoContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) } } @@ -25,14 +25,14 @@ public final class VoiceChatInfoContextItem: ContextMenuCustomItem { private final class VoiceChatInfoContextItemNode: ASDisplayNode, ContextMenuCustomNode { private let item: VoiceChatInfoContextItem private let presentationData: PresentationData - private let getController: () -> ContextController? + private let getController: () -> ContextControllerProtocol? private let actionSelected: (ContextMenuActionResult) -> Void private let backgroundNode: ASDisplayNode private let textNode: ImmediateTextNode private let iconNode: ASImageNode - init(presentationData: PresentationData, item: VoiceChatInfoContextItem, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + init(presentationData: PresentationData, item: VoiceChatInfoContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { self.item = item self.presentationData = presentationData self.getController = getController diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index d4fd270740..7686e94cb9 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -814,6 +814,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { if item.pinned { self.avatarNode.alpha = 1.0 videoNode.alpha = 0.0 + startContainerPosition = startContainerPosition.offsetBy(dx: 0.0, dy: 9.0) } else { self.avatarNode.alpha = 0.0 } @@ -971,6 +972,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { let currentItem = self.layoutParams?.0 let currentTitle = self.currentTitle + let hasVideo = self.videoNode != nil return { item, params, first, last in var updatedTheme: PresentationTheme? @@ -988,14 +990,16 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { if case .list = item.style, item.transparent{ titleFont = Font.semibold(17.0) titleColor = UIColor(rgb: 0xffffff, alpha: 0.65) - } else if case .tile = item.style { + } else if case .tile = item.style, !hasVideo { switch item.text { case let .text(_, _, textColor): switch textColor { case .generic: titleColor = item.presentationData.theme.list.itemPrimaryTextColor case .accent: - titleColor = item.presentationData.theme.list.itemAccentColor + if item.peer.id != item.context.account.peerId { + titleColor = item.presentationData.theme.list.itemAccentColor + } case .constructive: titleColor = constructiveColor case .destructive: @@ -1550,28 +1554,36 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { strongSelf.borderImageNode.isHidden = !item.pinned || item.style == .list + let canUpdateAvatarVisibility = !strongSelf.isExtracted && !strongSelf.animatingExtraction + if let videoNode = videoNode { let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) if !strongSelf.isExtracted && !strongSelf.animatingExtraction { if currentItem != nil { if case .tile = item.style { if item.pinned { + if strongSelf.avatarNode.alpha.isZero { + strongSelf.videoContainerNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2) + strongSelf.avatarNode.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2) + } transition.updateAlpha(node: videoNode, alpha: 0.0) transition.updateAlpha(node: strongSelf.videoFadeNode, alpha: 0.0) - strongSelf.videoContainerNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2) transition.updateAlpha(node: strongSelf.avatarNode, alpha: 1.0) - strongSelf.avatarNode.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2) } else { + if !strongSelf.avatarNode.alpha.isZero { + strongSelf.videoContainerNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) + strongSelf.avatarNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2) + } transition.updateAlpha(node: videoNode, alpha: 1.0) transition.updateAlpha(node: strongSelf.videoFadeNode, alpha: 1.0) - strongSelf.videoContainerNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) transition.updateAlpha(node: strongSelf.avatarNode, alpha: 0.0) - strongSelf.avatarNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2) } } else { if item.pinned { videoNode.alpha = 0.0 - strongSelf.avatarNode.alpha = 1.0 + if canUpdateAvatarVisibility { + strongSelf.avatarNode.alpha = 1.0 + } } else if strongSelf.videoReady { videoNode.alpha = 1.0 strongSelf.avatarNode.alpha = 0.0 @@ -1580,7 +1592,9 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { } else { if item.pinned { videoNode.alpha = 0.0 - strongSelf.avatarNode.alpha = 1.0 + if canUpdateAvatarVisibility { + strongSelf.avatarNode.alpha = 1.0 + } } else if strongSelf.videoReady { videoNode.alpha = 1.0 strongSelf.avatarNode.alpha = 0.0 @@ -1598,7 +1612,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { videoNode.position = CGPoint(x: videoSize.width / 2.0, y: videoSize.height / 2.0) videoNode.bounds = CGRect(origin: CGPoint(), size: videoSize) } - + if videoNodeUpdated { strongSelf.videoReadyDelayed = false strongSelf.videoReadyDisposable.set((videoNode.ready @@ -1608,13 +1622,18 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { strongSelf.videoReadyDelayed = true } strongSelf.videoReady = ready - if let videoNode = strongSelf.videoNode, ready && (strongSelf.item?.transparent != true) { + if let videoNode = strongSelf.videoNode, ready && !item.transparent { if strongSelf.videoReadyDelayed { Queue.mainQueue().after(0.15) { - switch item.style { + guard let currentItem = strongSelf.item else { + return + } + switch currentItem.style { case .list: - if item.pinned { - strongSelf.avatarNode.alpha = 1.0 + if currentItem.pinned { + if canUpdateAvatarVisibility { + strongSelf.avatarNode.alpha = 1.0 + } videoNode.alpha = 0.0 } else { strongSelf.avatarNode.alpha = 0.0 @@ -1623,8 +1642,10 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { videoNode.alpha = 1.0 } case .tile: - if item.pinned { - strongSelf.avatarNode.alpha = 1.0 + if currentItem.pinned { + if canUpdateAvatarVisibility { + strongSelf.avatarNode.alpha = 1.0 + } videoNode.alpha = 0.0 } else { strongSelf.avatarNode.alpha = 0.0 @@ -1636,7 +1657,9 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { } } else { if item.pinned { - strongSelf.avatarNode.alpha = 1.0 + if canUpdateAvatarVisibility { + strongSelf.avatarNode.alpha = 1.0 + } videoNode.alpha = 0.0 } else { strongSelf.avatarNode.alpha = 0.0 @@ -1647,18 +1670,18 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { } })) } - } else { + } else if canUpdateAvatarVisibility { strongSelf.avatarNode.alpha = 1.0 } switch item.style { case .list: strongSelf.audioLevelView?.alpha = item.transparent ? 0.0 : 1.0 - strongSelf.avatarNode.isHidden = item.transparent + strongSelf.avatarNode.isHidden = item.transparent || strongSelf.isExtracted strongSelf.videoContainerNode.isHidden = item.transparent strongSelf.pinIconNode.isHidden = !item.transparent if item.transparent && currentItem?.pinned != item.pinned { - strongSelf.pinIconNode.image = generateTintedImage(image: UIImage(bundleImageName: item.pinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: UIColor(rgb: 0xffffff)) + strongSelf.pinIconNode.image = generateTintedImage(image: UIImage(bundleImageName: item.pinned ? "Chat/Context Menu/Pin" : "Chat/Context Menu/Unpin"), color: UIColor(rgb: 0xffffff)) } case .tile: strongSelf.pinIconNode.isHidden = true diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift index ae86077f45..235c917d83 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift @@ -22,14 +22,14 @@ func generateStartRecordingIcon(color: UIColor) -> UIImage? { final class VoiceChatRecordingContextItem: ContextMenuCustomItem { fileprivate let timestamp: Int32 - fileprivate let action: (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void + fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void - init(timestamp: Int32, action: @escaping (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void) { + init(timestamp: Int32, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) { self.timestamp = timestamp self.action = action } - func node(presentationData: PresentationData, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { return VoiceChatRecordingContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) } } @@ -94,7 +94,7 @@ class VoiceChatRecordingIconNode: ASDisplayNode { private final class VoiceChatRecordingContextItemNode: ASDisplayNode, ContextMenuCustomNode { private let item: VoiceChatRecordingContextItem private let presentationData: PresentationData - private let getController: () -> ContextController? + private let getController: () -> ContextControllerProtocol? private let actionSelected: (ContextMenuActionResult) -> Void private let backgroundNode: ASDisplayNode @@ -108,7 +108,7 @@ private final class VoiceChatRecordingContextItemNode: ASDisplayNode, ContextMen private var pointerInteraction: PointerInteraction? - init(presentationData: PresentationData, item: VoiceChatRecordingContextItem, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + init(presentationData: PresentationData, item: VoiceChatRecordingContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { self.item = item self.presentationData = presentationData self.getController = getController diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift index 08fc213063..43e89a59ab 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift @@ -17,7 +17,7 @@ final class VoiceChatVolumeContextItem: ContextMenuCustomItem { self.valueChanged = valueChanged } - func node(presentationData: PresentationData, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { return VoiceChatVolumeContextItemNode(presentationData: presentationData, getController: getController, minValue: self.minValue, value: self.value, valueChanged: self.valueChanged) } } @@ -45,7 +45,7 @@ private final class VoiceChatVolumeContextItemNode: ASDisplayNode, ContextMenuCu private let hapticFeedback = HapticFeedback() - init(presentationData: PresentationData, getController: @escaping () -> ContextController?, minValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { + init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, minValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { self.presentationData = presentationData self.minValue = minValue self.value = value diff --git a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift index 4467fbb77f..727c893857 100644 --- a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift +++ b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift @@ -104,13 +104,6 @@ public extension AlertControllerTheme { } } -extension PeekControllerTheme { - convenience public init(presentationTheme: PresentationTheme) { - let actionSheet = presentationTheme.actionSheet - self.init(isDark: actionSheet.backgroundType == .dark, menuBackgroundColor: actionSheet.opaqueItemBackgroundColor, menuItemHighligtedColor: actionSheet.opaqueItemHighlightedBackgroundColor, menuItemSeparatorColor: actionSheet.opaqueItemSeparatorColor, accentColor: actionSheet.controlAccentColor, destructiveColor: actionSheet.destructiveActionTextColor) - } -} - public extension NavigationControllerTheme { convenience init(presentationTheme: PresentationTheme) { let navigationStatusBar: NavigationStatusBarStyle diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/Contents.json deleted file mode 100644 index eba3e80b71..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "ic_favesticker@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "ic_favesticker@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@2x.png b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@2x.png deleted file mode 100644 index 1a2b5d1035..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@3x.png b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@3x.png deleted file mode 100644 index b8aa993271..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/Contents.json deleted file mode 100644 index 7d96366e1c..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "ic_favedsticker@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "ic_favedsticker@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@2x.png b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@2x.png deleted file mode 100644 index 46f6daf33e..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@3x.png b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@3x.png deleted file mode 100644 index 16beedb0ec..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift index 8d02c2a15b..eaced07470 100644 --- a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift @@ -9,13 +9,14 @@ import SwiftSignalKit import AVFoundation import PhotoResources import AppBundle +import ContextUI final class ChatContextResultPeekContent: PeekControllerContent { let account: Account let contextResult: ChatContextResult - let menu: [PeekControllerMenuItem] + let menu: [ContextMenuItem] - init(account: Account, contextResult: ChatContextResult, menu: [PeekControllerMenuItem]) { + init(account: Account, contextResult: ChatContextResult, menu: [ContextMenuItem]) { self.account = account self.contextResult = contextResult self.menu = menu @@ -25,11 +26,11 @@ final class ChatContextResultPeekContent: PeekControllerContent { return .contained } - func menuActivation() -> PeerkControllerMenuActivation { + func menuActivation() -> PeerControllerMenuActivation { return .drag } - func menuItems() -> [PeekControllerMenuItem] { + func menuItems() -> [ContextMenuItem] { return self.menu } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 10f76a0fb2..e467c24954 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -11679,7 +11679,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions, contextController: ContextController?, completion: @escaping (ContextMenuActionResult) -> Void) { + private func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions, contextController: ContextControllerProtocol?, completion: @escaping (ContextMenuActionResult) -> Void) { let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] var personalPeerName: String? diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 2daa5baa46..7d2c72e85a 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -137,10 +137,6 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: LimitsCo return false } - -private let starIconEmpty = UIImage(bundleImageName: "Chat/Context Menu/StarIconEmpty")?.precomposed() -private let starIconFilled = UIImage(bundleImageName: "Chat/Context Menu/StarIconFilled")?.precomposed() - func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) -> Bool { guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else { return false @@ -1264,14 +1260,14 @@ func chatAvailableMessageActionsImpl(postbox: Postbox, accountPeerId: PeerId, me final class ChatDeleteMessageContextItem: ContextMenuCustomItem { fileprivate let timestamp: Double - fileprivate let action: (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void + fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void - init(timestamp: Double, action: @escaping (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void) { + init(timestamp: Double, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) { self.timestamp = timestamp self.action = action } - func node(presentationData: PresentationData, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { return ChatDeleteMessageContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) } } @@ -1281,7 +1277,7 @@ private let textFont = Font.regular(17.0) private final class ChatDeleteMessageContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol { private let item: ChatDeleteMessageContextItem private let presentationData: PresentationData - private let getController: () -> ContextController? + private let getController: () -> ContextControllerProtocol? private let actionSelected: (ContextMenuActionResult) -> Void private let backgroundNode: ASDisplayNode @@ -1296,7 +1292,7 @@ private final class ChatDeleteMessageContextItemNode: ASDisplayNode, ContextMenu private var pointerInteraction: PointerInteraction? - init(presentationData: PresentationData, item: ChatDeleteMessageContextItem, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + init(presentationData: PresentationData, item: ChatDeleteMessageContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { self.item = item self.presentationData = presentationData self.getController = getController diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index b1b8dabe90..2439a7793b 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -1023,6 +1023,33 @@ final class ChatMediaInputNode: ChatInputNode { let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect) } }))) + + if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = strongSelf.validLayout { + var isScheduledMessages = false + if case .scheduledMessages = interfaceState.subject { + isScheduledMessages = true + } + if !isScheduledMessages { + if case let .peer(peerId) = interfaceState.chatLocation { + if peerId != self?.context.account.peerId && peerId.namespace != Namespaces.Peer.SecretChat { + items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + + }))) + } + + items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + + }))) + } + } + } + if isSaved || isGifSaved { items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) @@ -1096,16 +1123,37 @@ final class ChatMediaInputNode: ChatInputNode { |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { - var menuItems: [PeekControllerMenuItem] = [] - menuItems = [ - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: { node, rect in - if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), nil, false, node, rect) - } else { - return false + var menuItems: [ContextMenuItem] = [] + if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = strongSelf.validLayout { + var isScheduledMessages = false + if case .scheduledMessages = interfaceState.subject { + isScheduledMessages = true + } + if !isScheduledMessages { + if case let .peer(peerId) = interfaceState.chatLocation { + if peerId != self?.context.account.peerId && peerId.namespace != Namespaces.Peer.SecretChat { + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + + }))) + } + + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + + }))) } - }), - PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in + } + } + + menuItems.append( + .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() @@ -1113,9 +1161,13 @@ final class ChatMediaInputNode: ChatInputNode { let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } - return true - }), - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + })) + ) + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + if let strongSelf = self { loop: for attribute in item.file.attributes { switch attribute { @@ -1138,10 +1190,9 @@ final class ChatMediaInputNode: ChatInputNode { } } } - return true - }), - PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }) - ] + }))) + + return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: item, menu: menuItems)) } else { return nil @@ -1180,16 +1231,37 @@ final class ChatMediaInputNode: ChatInputNode { |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { - var menuItems: [PeekControllerMenuItem] = [] - menuItems = [ - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: { node, rect in - if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), nil, false, node, rect) - } else { - return false + var menuItems: [ContextMenuItem] = [] + if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = strongSelf.validLayout { + var isScheduledMessages = false + if case .scheduledMessages = interfaceState.subject { + isScheduledMessages = true + } + if !isScheduledMessages { + if case let .peer(peerId) = interfaceState.chatLocation { + if peerId != self?.context.account.peerId && peerId.namespace != Namespaces.Peer.SecretChat { + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + + }))) + } + + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + + }))) } - }), - PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in + } + } + + menuItems.append( + .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() @@ -1197,35 +1269,38 @@ final class ChatMediaInputNode: ChatInputNode { let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } - return true - }), - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + })) + ) + menuItems.append( + .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + if let strongSelf = self { loop: for attribute in item.file.attributes { switch attribute { - case let .Sticker(_, packReference, _): - if let packReference = packReference { - let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in - if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(file, nil, false, sourceNode, sourceRect) - } else { - return false - } - }) - - strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true) - strongSelf.controllerInteraction.presentController(controller, nil) - } - break loop - default: - break + case let .Sticker(_, packReference, _): + if let packReference = packReference { + let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in + if let strongSelf = self { + return strongSelf.controllerInteraction.sendSticker(file, nil, false, sourceNode, sourceRect) + } else { + return false + } + }) + + strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true) + strongSelf.controllerInteraction.presentController(controller, nil) + } + break loop + default: + break } } } - return true - }), - PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }) - ] + })) + ) return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) } else { return nil @@ -1239,7 +1314,8 @@ final class ChatMediaInputNode: ChatInputNode { return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.theme), content: content, sourceNode: { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let controller = PeekController(presentationData: presentationData, content: content, sourceNode: { return sourceNode }) controller.visibilityUpdated = { [weak self] visible in diff --git a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift index a3866669e2..dd210b1147 100644 --- a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift @@ -55,9 +55,9 @@ final class ChatPanelInterfaceInteraction { let beginMessageSelection: ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void let deleteSelectedMessages: () -> Void let reportSelectedMessages: () -> Void - let reportMessages: ([Message], ContextController?) -> Void - let blockMessageAuthor: (Message, ContextController?) -> Void - let deleteMessages: ([Message], ContextController?, @escaping (ContextMenuActionResult) -> Void) -> Void + let reportMessages: ([Message], ContextControllerProtocol?) -> Void + let blockMessageAuthor: (Message, ContextControllerProtocol?) -> Void + let deleteMessages: ([Message], ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void let forwardSelectedMessages: () -> Void let forwardCurrentForwardMessages: () -> Void let forwardMessages: ([Message]) -> Void @@ -94,8 +94,8 @@ final class ChatPanelInterfaceInteraction { let setupMessageAutoremoveTimeout: () -> Void let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Bool let unblockPeer: () -> Void - let pinMessage: (MessageId, ContextController?) -> Void - let unpinMessage: (MessageId, Bool, ContextController?) -> Void + let pinMessage: (MessageId, ContextControllerProtocol?) -> Void + let unpinMessage: (MessageId, Bool, ContextControllerProtocol?) -> Void let unpinAllMessages: () -> Void let openPinnedList: (MessageId) -> Void let shareAccountContact: () -> Void @@ -138,9 +138,9 @@ final class ChatPanelInterfaceInteraction { beginMessageSelection: @escaping ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void, deleteSelectedMessages: @escaping () -> Void, reportSelectedMessages: @escaping () -> Void, - reportMessages: @escaping ([Message], ContextController?) -> Void, - blockMessageAuthor: @escaping (Message, ContextController?) -> Void, - deleteMessages: @escaping ([Message], ContextController?, @escaping (ContextMenuActionResult) -> Void) -> Void, + reportMessages: @escaping ([Message], ContextControllerProtocol?) -> Void, + blockMessageAuthor: @escaping (Message, ContextControllerProtocol?) -> Void, + deleteMessages: @escaping ([Message], ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardCurrentForwardMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, @@ -177,8 +177,8 @@ final class ChatPanelInterfaceInteraction { setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, unblockPeer: @escaping () -> Void, - pinMessage: @escaping (MessageId, ContextController?) -> Void, - unpinMessage: @escaping (MessageId, Bool, ContextController?) -> Void, + pinMessage: @escaping (MessageId, ContextControllerProtocol?) -> Void, + unpinMessage: @escaping (MessageId, Bool, ContextControllerProtocol?) -> Void, unpinAllMessages: @escaping () -> Void, openPinnedList: @escaping (MessageId) -> Void, shareAccountContact: @escaping () -> Void, diff --git a/submodules/TelegramUI/Sources/ChatScheduleTimeControllerNode.swift b/submodules/TelegramUI/Sources/ChatScheduleTimeControllerNode.swift index 0233a7eafc..fee537ce93 100644 --- a/submodules/TelegramUI/Sources/ChatScheduleTimeControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatScheduleTimeControllerNode.swift @@ -314,10 +314,16 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + let targetBounds = self.bounds + self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) + self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) + transition.animateView({ + self.bounds = targetBounds + self.dimNode.position = dimPosition + }) } func animateOut(completion: (() -> Void)? = nil) { diff --git a/submodules/TelegramUI/Sources/ChatTimerScreen.swift b/submodules/TelegramUI/Sources/ChatTimerScreen.swift index 2ccb917a2b..a529d71497 100644 --- a/submodules/TelegramUI/Sources/ChatTimerScreen.swift +++ b/submodules/TelegramUI/Sources/ChatTimerScreen.swift @@ -401,10 +401,16 @@ class ChatTimerScreenNode: ViewControllerTracingNode, UIScrollViewDelegate, UIPi self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + let targetBounds = self.bounds + self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) + self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) + transition.animateView({ + self.bounds = targetBounds + self.dimNode.position = dimPosition + }) } func animateOut(completion: (() -> Void)? = nil) { diff --git a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift index b6a8681938..fcaac82741 100644 --- a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift +++ b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift @@ -15,6 +15,7 @@ import OverlayStatusController import PresentationDataUtils import SearchBarNode import UndoUI +import ContextUI private final class FeaturedInteraction { let installPack: (ItemCollectionInfo, Bool) -> Void @@ -460,16 +461,16 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] menuItems = [ - PeekControllerMenuItem(title: strongSelf.presentationData.strings.StickerPack_Send, color: .accent, font: .bold, action: { node, rect in - if let strongSelf = self { - return strongSelf.sendSticker?(.standalone(media: item.file), node, rect) ?? false - } else { - return false - } - }), - PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + +// let _ = strongSelf.sendSticker?(.standalone(media: item.file), node, rect) + })), + .action(ContextMenuActionItem(text: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() @@ -477,9 +478,10 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } - return true - }), - PeekControllerMenuItem(title: strongSelf.presentationData.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + })), + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { loop: for attribute in item.file.attributes { switch attribute { @@ -502,9 +504,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { } } } - return true - }), - PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }) + })) ] return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: item, menu: menuItems)) } else { @@ -524,16 +524,16 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] menuItems = [ - PeekControllerMenuItem(title: strongSelf.presentationData.strings.StickerPack_Send, color: .accent, font: .bold, action: { node, rect in - if let strongSelf = self { - return strongSelf.sendSticker?(.standalone(media: item.file), node, rect) ?? false - } else { - return false - } - }), - PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + +// let _ = strongSelf.sendSticker?(.standalone(media: item.file), node, rect) + })), + .action(ContextMenuActionItem(text: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() @@ -541,34 +541,33 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } - return true - }), - PeekControllerMenuItem(title: strongSelf.presentationData.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + })), + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { loop: for attribute in item.file.attributes { switch attribute { - case let .Sticker(_, packReference, _): - if let packReference = packReference { - let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controller?.navigationController as? NavigationController, sendSticker: { file, sourceNode, sourceRect in - if let strongSelf = self { - return strongSelf.sendSticker?(file, sourceNode, sourceRect) ?? false - } else { - return false - } - }) - - strongSelf.controller?.view.endEditing(true) - strongSelf.controller?.present(controller, in: .window(.root)) - } - break loop - default: - break + case let .Sticker(_, packReference, _): + if let packReference = packReference { + let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controller?.navigationController as? NavigationController, sendSticker: { file, sourceNode, sourceRect in + if let strongSelf = self { + return strongSelf.sendSticker?(file, sourceNode, sourceRect) ?? false + } else { + return false + } + }) + + strongSelf.controller?.view.endEditing(true) + strongSelf.controller?.present(controller, in: .window(.root)) + } + break loop + default: + break } } } - return true - }), - PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }) + })) ] return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) } else { @@ -579,7 +578,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.presentationData.theme), content: content, sourceNode: { + let controller = PeekController(presentationData: strongSelf.presentationData, content: content, sourceNode: { return sourceNode }) strongSelf.controller?.presentInGlobalOverlay(controller) diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift index 962fb53c3a..e55f3dab37 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -11,6 +11,7 @@ import TelegramUIPreferences import MergeLists import AccountContext import StickerPackPreviewUI +import ContextUI private struct ChatContextResultStableId: Hashable { let result: ChatContextResult @@ -145,15 +146,19 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont strongSelf.listView.forEachItemNode { itemNode in if itemNode.frame.contains(convertedPoint), let itemNode = itemNode as? HorizontalListContextResultsChatInputPanelItemNode, let item = itemNode.item { if case let .internalReference(internalReference) = item.result, let file = internalReference.file, file.isSticker { - var menuItems: [PeekControllerMenuItem] = [] - menuItems.append(PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: { _, _ in - return item.resultSelected(item.result, itemNode, itemNode.bounds) - })) + var menuItems: [ContextMenuItem] = [] + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + let _ = item.resultSelected(item.result, itemNode, itemNode.bounds) + }))) for case let .Sticker(_, packReference, _) in file.attributes { guard let packReference = packReference else { continue } - menuItems.append(PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.interfaceInteraction?.getNavigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self { @@ -166,21 +171,29 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont strongSelf.interfaceInteraction?.getNavigationController()?.view.window?.endEditing(true) strongSelf.interfaceInteraction?.presentController(controller, nil) } - return true - })) + }))) } selectedItemNodeAndContent = (itemNode, StickerPreviewPeekContent(account: item.account, item: .found(FoundStickerItem(file: file, stringRepresentations: [])), menu: menuItems)) } else { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] if case let .internalReference(internalReference) = item.result, let file = internalReference.file, file.isAnimated { - menuItems.append(PeekControllerMenuItem(title: strongSelf.strings.Preview_SaveGif, color: .accent, action: { _, _ in + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Preview_SaveGif, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.dismissWithoutContent) + + guard let strongSelf = self else { + return + } let _ = addSavedGif(postbox: strongSelf.context.account.postbox, fileReference: .standalone(media: file)).start() - return true - })) + }))) } - menuItems.append(PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, font: .bold, action: { _, _ in - return item.resultSelected(item.result, itemNode, itemNode.bounds) - })) + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.ShareMenu_Send, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + item.resultSelected(item.result, itemNode, itemNode.bounds) + }))) selectedItemNodeAndContent = (itemNode, ChatContextResultPeekContent(account: item.account, contextResult: item.result, menu: menuItems)) } } @@ -190,7 +203,8 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.theme), content: content, sourceNode: { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let controller = PeekController(presentationData: presentationData, content: content, sourceNode: { return sourceNode }) strongSelf.interfaceInteraction?.presentGlobalOverlayController(controller, nil) diff --git a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift index 462ba94af1..844ef3932d 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift @@ -11,6 +11,7 @@ import TelegramUIPreferences import MergeLists import AccountContext import StickerPackPreviewUI +import ContextUI final class HorizontalStickersChatContextPanelInteraction { var previewedStickerItem: StickerPackItem? @@ -174,12 +175,16 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] menuItems = [ - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: { _, _ in - return controllerInteraction.sendSticker(.standalone(media: item.file), nil, true, itemNode, itemNode.bounds) - }), - PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in + .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), nil, true, itemNode, itemNode.bounds) + })), + .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() @@ -187,9 +192,10 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } - return true - }), - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + })), + .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { loop: for attribute in item.file.attributes { switch attribute { @@ -211,11 +217,8 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { break } } - return true } - return true - }), - PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }) + })) ] return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) } else { @@ -227,7 +230,8 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.theme), content: content, sourceNode: { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let controller = PeekController(presentationData: presentationData, content: content, sourceNode: { return sourceNode }) strongSelf.interfaceInteraction?.presentGlobalOverlayController(controller, nil) diff --git a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift index 817dcfd341..5e4d4663d4 100644 --- a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift +++ b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift @@ -10,6 +10,7 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext import StickerPackPreviewUI +import ContextUI private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollViewDelegate { private final class DisplayItem { @@ -88,12 +89,16 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self, let controllerInteraction = strongSelf.getControllerInteraction?() { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] menuItems = [ - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: { _, _ in - return controllerInteraction.sendSticker(.standalone(media: item.file), nil, true, itemNode, itemNode.bounds) - }), - PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in + .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), nil, true, itemNode, itemNode.bounds) + })), + .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() @@ -101,9 +106,10 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } - return true - }), - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + })), + .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self, let controllerInteraction = strongSelf.getControllerInteraction?() { loop: for attribute in item.file.attributes { switch attribute { @@ -125,12 +131,8 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie break } } - return true } - return true - }), - PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }) - ] + }))] return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) } else { return nil @@ -141,7 +143,8 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.theme), content: content, sourceNode: { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let controller = PeekController(presentationData: presentationData, content: content, sourceNode: { return sourceNode }) strongSelf.getControllerInteraction?()?.presentGlobalOverlayController(controller, nil) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 0b8fb13031..cc2caf7b86 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3911,7 +3911,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.controller?.present(shareController, in: .window(.root)) } - private func requestCall(isVideo: Bool, gesture: ContextGesture? = nil, contextController: ContextController? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextController) -> Void)? = nil) { + private func requestCall(isVideo: Bool, gesture: ContextGesture? = nil, contextController: ContextControllerProtocol? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextControllerProtocol) -> Void)? = nil) { let peerId = self.peerId let requestCall: (PeerId?, CachedChannelData.ActiveCall?) -> Void = { [weak self] defaultJoinAsPeerId, activeCall in if let activeCall = activeCall { @@ -4310,7 +4310,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD controller.push(statsController) } - private func openVoiceChatOptions(defaultJoinAsPeerId: PeerId?, gesture: ContextGesture? = nil, contextController: ContextController? = nil) { + private func openVoiceChatOptions(defaultJoinAsPeerId: PeerId?, gesture: ContextGesture? = nil, contextController: ContextControllerProtocol? = nil) { let context = self.context let peerId = self.peerId let defaultJoinAsPeerId = defaultJoinAsPeerId ?? self.context.account.peerId @@ -4392,7 +4392,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }) } - private func openVoiceChatDisplayAsPeerSelection(completion: @escaping (PeerId) -> Void, gesture: ContextGesture? = nil, contextController: ContextController? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextController) -> Void)? = nil) { + private func openVoiceChatDisplayAsPeerSelection(completion: @escaping (PeerId) -> Void, gesture: ContextGesture? = nil, contextController: ContextControllerProtocol? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextControllerProtocol) -> Void)? = nil) { let dismissOnSelection = contextController == nil let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(context.account.peerId) |> map { peer in @@ -4489,7 +4489,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }) } - private func openReport(user: Bool, contextController: ContextController?, backAction: ((ContextController) -> Void)?) { + private func openReport(user: Bool, contextController: ContextControllerProtocol?, backAction: ((ContextControllerProtocol) -> Void)?) { guard let controller = self.controller else { return } diff --git a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift index 7881a01849..b69e5daa0a 100644 --- a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift @@ -11,6 +11,7 @@ import TelegramUIPreferences import MergeLists import AccountContext import StickerPackPreviewUI +import ContextUI private struct StickersChatInputContextPanelEntryStableId: Hashable { let ids: [MediaId] @@ -130,12 +131,16 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] menuItems = [ - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: { _, _ in - return controllerInteraction.sendSticker(.standalone(media: item.file), nil, true, itemNode, itemNode.bounds) - }), - PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in + .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), nil, true, itemNode, itemNode.bounds) + })), + .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() @@ -143,9 +148,10 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } - return true - }), - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + })), + .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { loop: for attribute in item.file.attributes { switch attribute { @@ -157,7 +163,6 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { } else { return false } - }) controllerInteraction.navigationController()?.view.window?.endEditing(true) @@ -169,9 +174,7 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { } } } - return true - }), - PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }) + })) ] return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) } else { @@ -184,7 +187,8 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.theme), content: content, sourceNode: { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let controller = PeekController(presentationData: presentationData, content: content, sourceNode: { return sourceNode }) strongSelf.interfaceInteraction?.presentGlobalOverlayController(controller, nil)