mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Web app minimization
This commit is contained in:
parent
7d5d9ba7ba
commit
2b1d09fb5a
@ -12284,6 +12284,7 @@ Sorry for the inconvenience.";
|
|||||||
"Stars.Purchase.StarsNeeded_1" = "%@ Star Needed";
|
"Stars.Purchase.StarsNeeded_1" = "%@ Star Needed";
|
||||||
"Stars.Purchase.StarsNeeded_any" = "%@ Stars Needed";
|
"Stars.Purchase.StarsNeeded_any" = "%@ Stars Needed";
|
||||||
"Stars.Purchase.StarsNeededInfo" = "Buy Stars to use them on **%@** and other miniapps.";
|
"Stars.Purchase.StarsNeededInfo" = "Buy Stars to use them on **%@** and other miniapps.";
|
||||||
|
"Stars.Purchase.StarsNeededUnlockInfo" = "Buy Stars to unlock media and use them on miniapps.";
|
||||||
|
|
||||||
"Stars.Purchase.Stars_1" = "%@ Star";
|
"Stars.Purchase.Stars_1" = "%@ Star";
|
||||||
"Stars.Purchase.Stars_any" = "%@ Stars";
|
"Stars.Purchase.Stars_any" = "%@ Stars";
|
||||||
@ -12453,10 +12454,15 @@ Sorry for the inconvenience.";
|
|||||||
"Premium.MessageEffects" = "Message Effects";
|
"Premium.MessageEffects" = "Message Effects";
|
||||||
"Premium.MessageEffectsInfo" = "Add over 500 animated effects to private messages.";
|
"Premium.MessageEffectsInfo" = "Add over 500 animated effects to private messages.";
|
||||||
|
|
||||||
"Chat.UnlockMedia" = "Unlock for %@";
|
"Chat.PaidMedia.UnlockMedia" = "Unlock for %@";
|
||||||
|
"Chat.PaidMedia.Purchased" = "Purchased";
|
||||||
|
|
||||||
"Attachment.SendWithoutGrouping" = "Send Without Grouping";
|
"Attachment.SendWithoutGrouping" = "Send Without Grouping";
|
||||||
"Attachment.Paid.EditPrice" = "Edit Price";
|
"Attachment.Paid.EditPrice" = "Edit Price";
|
||||||
"Attachment.Paid.EditPrice.Stars_1" = "%@ Star";
|
"Attachment.Paid.EditPrice.Stars_1" = "%@ Star";
|
||||||
"Attachment.Paid.EditPrice.Stars_any" = "%@ Stars";
|
"Attachment.Paid.EditPrice.Stars_any" = "%@ Stars";
|
||||||
"Attachment.Paid.Create" = "Make This Content Paid";
|
"Attachment.Paid.Create" = "Make This Content Paid";
|
||||||
|
|
||||||
|
"WebApp.MinimizedTitleFormat" = "%1$@ & %2$@";
|
||||||
|
"WebApp.MinimizedTitle.Others_1" = "%@ Other";
|
||||||
|
"WebApp.MinimizedTitle.Others_any" = "%@ Others";
|
||||||
|
@ -41,6 +41,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView",
|
"//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView",
|
||||||
"//submodules/ReactionSelectionNode",
|
"//submodules/ReactionSelectionNode",
|
||||||
"//submodules/TelegramUI/Components/Chat/TopMessageReactions",
|
"//submodules/TelegramUI/Components/Chat/TopMessageReactions",
|
||||||
|
"//submodules/TelegramUI/Components/MinimizedContainer",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -34,13 +34,13 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate {
|
|||||||
private(set) var dismissProgress: CGFloat = 0.0
|
private(set) var dismissProgress: CGFloat = 0.0
|
||||||
var isReadyUpdated: (() -> Void)?
|
var isReadyUpdated: (() -> Void)?
|
||||||
var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||||
var interactivelyDismissed: (() -> Bool)?
|
var interactivelyDismissed: ((CGFloat) -> Bool)?
|
||||||
var controllerRemoved: ((ViewController) -> Void)?
|
var controllerRemoved: ((ViewController) -> Void)?
|
||||||
|
|
||||||
var shouldCancelPanGesture: (() -> Bool)?
|
var shouldCancelPanGesture: (() -> Bool)?
|
||||||
var requestDismiss: (() -> Void)?
|
var requestDismiss: (() -> Void)?
|
||||||
|
|
||||||
var updateModalProgress: ((CGFloat, CGFloat, ContainedViewLayoutTransition) -> Void)?
|
var updateModalProgress: ((CGFloat, CGFloat, CGRect, ContainedViewLayoutTransition) -> Void)?
|
||||||
|
|
||||||
private var isUpdatingState = false
|
private var isUpdatingState = false
|
||||||
private var isDismissed = false
|
private var isDismissed = false
|
||||||
@ -306,10 +306,13 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate {
|
|||||||
ignoreDismiss = true
|
ignoreDismiss = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var minimizing = false
|
||||||
var dismissing = false
|
var dismissing = false
|
||||||
if (bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0)) && !ignoreDismiss {
|
if (bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0)) && !ignoreDismiss {
|
||||||
if self.interactivelyDismissed?() == true {
|
if self.interactivelyDismissed?(velocity.y) == true {
|
||||||
dismissing = true
|
dismissing = true
|
||||||
|
} else {
|
||||||
|
minimizing = true
|
||||||
}
|
}
|
||||||
} else if self.isExpanded {
|
} else if self.isExpanded {
|
||||||
if velocity.y > 300.0 || offset > topInset / 2.0 {
|
if velocity.y > 300.0 || offset > topInset / 2.0 {
|
||||||
@ -363,7 +366,9 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate {
|
|||||||
let previousBounds = bounds
|
let previousBounds = bounds
|
||||||
bounds.origin.y = 0.0
|
bounds.origin.y = 0.0
|
||||||
self.bounds = bounds
|
self.bounds = bounds
|
||||||
self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
if !minimizing {
|
||||||
|
self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .cancelled:
|
case .cancelled:
|
||||||
self.panGestureArguments = nil
|
self.panGestureArguments = nil
|
||||||
@ -391,8 +396,8 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) {
|
func update(isExpanded: Bool, force: Bool = false, transition: ContainedViewLayoutTransition) {
|
||||||
guard isExpanded != self.isExpanded else {
|
guard isExpanded != self.isExpanded || force else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.isExpanded = isExpanded
|
self.isExpanded = isExpanded
|
||||||
@ -437,7 +442,7 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate {
|
|||||||
})
|
})
|
||||||
|
|
||||||
let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / defaultTopInset)
|
let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / defaultTopInset)
|
||||||
self.updateModalProgress?(modalProgress, topInset, transition)
|
self.updateModalProgress?(modalProgress, topInset, self.bounds, transition)
|
||||||
|
|
||||||
let containerLayout: ContainerViewLayout
|
let containerLayout: ContainerViewLayout
|
||||||
let containerFrame: CGRect
|
let containerFrame: CGRect
|
||||||
|
@ -15,6 +15,7 @@ import LegacyMessageInputPanel
|
|||||||
import LegacyMessageInputPanelInputView
|
import LegacyMessageInputPanelInputView
|
||||||
import AttachmentTextInputPanelNode
|
import AttachmentTextInputPanelNode
|
||||||
import ChatSendMessageActionUI
|
import ChatSendMessageActionUI
|
||||||
|
import MinimizedContainer
|
||||||
|
|
||||||
public enum AttachmentButtonType: Equatable {
|
public enum AttachmentButtonType: Equatable {
|
||||||
case gallery
|
case gallery
|
||||||
@ -342,7 +343,7 @@ public class AttachmentController: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.container.updateModalProgress = { [weak self] progress, topInset, transition in
|
self.container.updateModalProgress = { [weak self] progress, topInset, bounds, transition in
|
||||||
if let strongSelf = self, let layout = strongSelf.validLayout, !strongSelf.isDismissing {
|
if let strongSelf = self, let layout = strongSelf.validLayout, !strongSelf.isDismissing {
|
||||||
var transition = transition
|
var transition = transition
|
||||||
if strongSelf.container.supernode == nil {
|
if strongSelf.container.supernode == nil {
|
||||||
@ -350,7 +351,8 @@ public class AttachmentController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.modalProgress = progress
|
strongSelf.modalProgress = progress
|
||||||
strongSelf.controller?.modalTopEdgeOffset = topInset
|
strongSelf.controller?.minimizedTopEdgeOffset = topInset
|
||||||
|
strongSelf.controller?.minimizedBounds = bounds
|
||||||
|
|
||||||
if !strongSelf.isMinimizing {
|
if !strongSelf.isMinimizing {
|
||||||
strongSelf.controller?.updateModalStyleOverlayTransitionFactor(progress, transition: transition)
|
strongSelf.controller?.updateModalStyleOverlayTransitionFactor(progress, transition: transition)
|
||||||
@ -364,16 +366,37 @@ public class AttachmentController: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.container.interactivelyDismissed = { [weak self] in
|
self.container.interactivelyDismissed = { [weak self] velocity in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self, let layout = strongSelf.validLayout {
|
||||||
if let controller = strongSelf.controller, controller.shouldMinimizeOnSwipe?() == true, let navigationController = controller.navigationController as? NavigationController {
|
if let controller = strongSelf.controller, controller.shouldMinimizeOnSwipe?() == true, let navigationController = controller.navigationController as? NavigationController {
|
||||||
navigationController.minimizeViewController(controller, animated: true)
|
|
||||||
|
let delta = layout.size.height - controller.minimizedTopEdgeOffset
|
||||||
|
let damping: CGFloat = 180
|
||||||
|
let initialVelocity: CGFloat = delta > 0.0 ? velocity / delta : 0.0
|
||||||
|
|
||||||
|
navigationController.minimizeViewController(controller, damping: damping, velocity: initialVelocity, setupContainer: { [weak self] current in
|
||||||
|
let minimizedContainer: MinimizedContainerImpl?
|
||||||
|
if let current = current as? MinimizedContainerImpl {
|
||||||
|
minimizedContainer = current
|
||||||
|
} else if let context = self?.controller?.context {
|
||||||
|
minimizedContainer = MinimizedContainerImpl(context: context, navigationController: navigationController)
|
||||||
|
} else {
|
||||||
|
minimizedContainer = nil
|
||||||
|
}
|
||||||
|
return minimizedContainer
|
||||||
|
}, animated: true)
|
||||||
|
|
||||||
|
strongSelf.dim.isHidden = true
|
||||||
|
|
||||||
|
strongSelf.isMinimizing = true
|
||||||
|
strongSelf.container.update(isExpanded: true, force: true, transition: .immediate)
|
||||||
|
// strongSelf.container.update(isExpanded: true, force: true, transition: .animated(duration: 0.4, curve: .customSpring(damping: 180.0, initialVelocity: initialVelocity)))
|
||||||
|
strongSelf.isMinimizing = false
|
||||||
|
|
||||||
|
Queue.mainQueue().after(0.45, {
|
||||||
|
strongSelf.dim.isHidden = false
|
||||||
|
})
|
||||||
|
|
||||||
Queue.mainQueue().after(0.5) {
|
|
||||||
strongSelf.isMinimizing = true
|
|
||||||
strongSelf.container.update(isExpanded: true, transition: .immediate)
|
|
||||||
strongSelf.isMinimizing = false
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
strongSelf.controller?.dismiss(animated: true)
|
strongSelf.controller?.dismiss(animated: true)
|
||||||
@ -1038,13 +1061,31 @@ public class AttachmentController: ViewController {
|
|||||||
return self.buttons.contains(.standalone)
|
return self.buttons.contains(.standalone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var snapshotView: UIView?
|
||||||
public override var isMinimized: Bool {
|
public override var isMinimized: Bool {
|
||||||
didSet {
|
didSet {
|
||||||
guard self.isMinimized != oldValue else {
|
guard self.isMinimized != oldValue else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if self.isMinimized {
|
||||||
|
if self.snapshotView == nil, let lastController = self.node.container.container.controllers.last, let snapshotView = lastController.view.snapshotView(afterScreenUpdates: false) {
|
||||||
|
snapshotView.isUserInteractionEnabled = false
|
||||||
|
self.snapshotView = snapshotView
|
||||||
|
lastController.view.addSubview(snapshotView)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let snapshotView = self.snapshotView {
|
||||||
|
self.snapshotView = nil
|
||||||
|
Queue.mainQueue().after(0.5) {
|
||||||
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||||
|
snapshotView.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !self.node.isDismissing {
|
if !self.node.isDismissing {
|
||||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
let transition: ContainedViewLayoutTransition = self.isMinimized ? .immediate : .animated(duration: 0.2, curve: .easeInOut)
|
||||||
transition.updateAlpha(node: self.node.dim, alpha: self.isMinimized ? 0.0 : 1.0)
|
transition.updateAlpha(node: self.node.dim, alpha: self.isMinimized ? 0.0 : 1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,445 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import AsyncDisplayKit
|
|
||||||
import Display
|
|
||||||
import AppBundle
|
|
||||||
|
|
||||||
let maxInteritemSpacing: CGFloat = 240.0
|
|
||||||
let sectionInsetTop: CGFloat = 40.0
|
|
||||||
let sectionInsetBottom: CGFloat = 0.0
|
|
||||||
let zOffset: CGFloat = -60.0
|
|
||||||
|
|
||||||
let perspectiveCorrection: CGFloat = -1.0 / 1000.0
|
|
||||||
let maxRotationAngle: CGFloat = -CGFloat.pi / 2.2
|
|
||||||
|
|
||||||
extension CATransform3D {
|
|
||||||
func interpolate(other: CATransform3D, progress: CGFloat) -> CATransform3D {
|
|
||||||
var vectors = Array<CGFloat>(repeating: 0.0, count: 16)
|
|
||||||
vectors[0] = self.m11 + (other.m11 - self.m11) * progress
|
|
||||||
vectors[1] = self.m12 + (other.m12 - self.m12) * progress
|
|
||||||
vectors[2] = self.m13 + (other.m13 - self.m13) * progress
|
|
||||||
vectors[3] = self.m14 + (other.m14 - self.m14) * progress
|
|
||||||
vectors[4] = self.m21 + (other.m21 - self.m21) * progress
|
|
||||||
vectors[5] = self.m22 + (other.m22 - self.m22) * progress
|
|
||||||
vectors[6] = self.m23 + (other.m23 - self.m23) * progress
|
|
||||||
vectors[7] = self.m24 + (other.m24 - self.m24) * progress
|
|
||||||
vectors[8] = self.m31 + (other.m31 - self.m31) * progress
|
|
||||||
vectors[9] = self.m32 + (other.m32 - self.m32) * progress
|
|
||||||
vectors[10] = self.m33 + (other.m33 - self.m33) * progress
|
|
||||||
vectors[11] = self.m34 + (other.m34 - self.m34) * progress
|
|
||||||
vectors[12] = self.m41 + (other.m41 - self.m41) * progress
|
|
||||||
vectors[13] = self.m42 + (other.m42 - self.m42) * progress
|
|
||||||
vectors[14] = self.m43 + (other.m43 - self.m43) * progress
|
|
||||||
vectors[15] = self.m44 + (other.m44 - self.m44) * progress
|
|
||||||
|
|
||||||
return CATransform3D(m11: vectors[0], m12: vectors[1], m13: vectors[2], m14: vectors[3], m21: vectors[4], m22: vectors[5], m23: vectors[6], m24: vectors[7], m31: vectors[8], m32: vectors[9], m33: vectors[10], m34: vectors[11], m41: vectors[12], m42: vectors[13], m43: vectors[14], m44: vectors[15])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private func angle(for origin: CGFloat, itemCount: Int, bounds: CGRect, contentHeight: CGFloat?) -> CGFloat {
|
|
||||||
var rotationAngle = rotationAngleAt0(itemCount: itemCount)
|
|
||||||
|
|
||||||
var contentOffset = bounds.origin.y
|
|
||||||
if contentOffset < 0.0 {
|
|
||||||
contentOffset *= 2.0
|
|
||||||
}
|
|
||||||
// } else if let contentHeight = contentHeight, bounds.maxY > contentHeight {
|
|
||||||
//// let maxContentOffset = contentHeight - bounds.height
|
|
||||||
//// let delta = contentOffset - maxContentOffset
|
|
||||||
//// contentOffset = maxContentOffset + delta / 2.0
|
|
||||||
// }
|
|
||||||
|
|
||||||
var yOnScreen = origin - contentOffset - sectionInsetTop
|
|
||||||
if yOnScreen < 0 {
|
|
||||||
yOnScreen = 0
|
|
||||||
} else if yOnScreen > bounds.height {
|
|
||||||
yOnScreen = bounds.height
|
|
||||||
}
|
|
||||||
|
|
||||||
let maxRotationVariance = maxRotationAngle - rotationAngleAt0(itemCount: itemCount)
|
|
||||||
rotationAngle += (maxRotationVariance / bounds.height) * yOnScreen
|
|
||||||
|
|
||||||
return rotationAngle
|
|
||||||
}
|
|
||||||
|
|
||||||
private func final3dTransform(for origin: CGFloat, size: CGSize, contentHeight: CGFloat?, itemCount: Int, forcedAngle: CGFloat? = nil, additionalAngle: CGFloat? = nil, bounds: CGRect) -> CATransform3D {
|
|
||||||
var transform = CATransform3DIdentity
|
|
||||||
transform.m34 = perspectiveCorrection
|
|
||||||
|
|
||||||
let rotationAngle = forcedAngle ?? angle(for: origin, itemCount: itemCount, bounds: bounds, contentHeight: contentHeight)
|
|
||||||
var effectiveRotationAngle = rotationAngle
|
|
||||||
if let additionalAngle = additionalAngle {
|
|
||||||
effectiveRotationAngle += additionalAngle
|
|
||||||
}
|
|
||||||
|
|
||||||
let r = size.height / 2.0 + abs(zOffset / sin(rotationAngle))
|
|
||||||
|
|
||||||
let zTranslation = r * sin(rotationAngle)
|
|
||||||
let yTranslation: CGFloat = r * (1 - cos(rotationAngle))
|
|
||||||
|
|
||||||
let zTranslateTransform = CATransform3DTranslate(transform, 0.0, -yTranslation, zTranslation)
|
|
||||||
|
|
||||||
let rotateTransform = CATransform3DRotate(zTranslateTransform, effectiveRotationAngle, 1.0, 0.0, 0.0)
|
|
||||||
|
|
||||||
return rotateTransform
|
|
||||||
}
|
|
||||||
|
|
||||||
private func interitemSpacing(itemCount: Int, bounds: CGRect) -> CGFloat {
|
|
||||||
var interitemSpacing = maxInteritemSpacing
|
|
||||||
if itemCount > 0 {
|
|
||||||
interitemSpacing = (bounds.height - sectionInsetTop - sectionInsetBottom) / CGFloat(min(itemCount, 5))
|
|
||||||
}
|
|
||||||
return interitemSpacing
|
|
||||||
}
|
|
||||||
|
|
||||||
private func frameForIndex(index: Int, size: CGSize, itemCount: Int, bounds: CGRect) -> CGRect {
|
|
||||||
let spacing = interitemSpacing(itemCount: itemCount, bounds: bounds)
|
|
||||||
let y = sectionInsetTop + spacing * CGFloat(index)
|
|
||||||
let origin = CGPoint(x: 0, y: y)
|
|
||||||
|
|
||||||
return CGRect(origin: origin, size: size)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func rotationAngleAt0(itemCount: Int) -> CGFloat {
|
|
||||||
let multiplier: CGFloat = min(CGFloat(itemCount), 5.0) - 1.0
|
|
||||||
return -CGFloat.pi / 7.0 - CGFloat.pi / 7.0 * multiplier / 4.0
|
|
||||||
}
|
|
||||||
|
|
||||||
private let shadowImage: UIImage? = {
|
|
||||||
return generateImage(CGSize(width: 1.0, height: 640.0), rotatedContext: { size, context in
|
|
||||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
|
||||||
context.clear(bounds)
|
|
||||||
|
|
||||||
let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor] as CFArray
|
|
||||||
|
|
||||||
var locations: [CGFloat] = [0.0, 0.65, 1.0]
|
|
||||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
|
||||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
|
||||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: bounds.height), options: [])
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
|
|
||||||
class StackItemContainerNode: ASDisplayNode {
|
|
||||||
private let node: ASDisplayNode
|
|
||||||
private let shadowNode: ASImageNode
|
|
||||||
|
|
||||||
var tapped: (() -> Void)?
|
|
||||||
var highlighted: ((Bool) -> Void)?
|
|
||||||
|
|
||||||
init(node: ASDisplayNode) {
|
|
||||||
self.node = node
|
|
||||||
self.shadowNode = ASImageNode()
|
|
||||||
self.shadowNode.displaysAsynchronously = false
|
|
||||||
self.shadowNode.displayWithoutProcessing = true
|
|
||||||
self.shadowNode.contentMode = .scaleToFill
|
|
||||||
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
self.clipsToBounds = true
|
|
||||||
self.cornerRadius = 10.0
|
|
||||||
applySmoothRoundedCorners(self.layer)
|
|
||||||
|
|
||||||
self.shadowNode.image = shadowImage
|
|
||||||
|
|
||||||
self.addSubnode(self.node)
|
|
||||||
self.addSubnode(self.shadowNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func didLoad() {
|
|
||||||
super.didLoad()
|
|
||||||
|
|
||||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
|
||||||
recognizer.tapActionAtPoint = { point in
|
|
||||||
return .waitForSingleTap
|
|
||||||
}
|
|
||||||
recognizer.highlight = { [weak self] point in
|
|
||||||
if let point = point, point.x > 280.0 {
|
|
||||||
self?.highlighted?(true)
|
|
||||||
} else {
|
|
||||||
self?.highlighted?(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.view.addGestureRecognizer(recognizer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func animateIn() {
|
|
||||||
self.shadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
|
||||||
switch recognizer.state {
|
|
||||||
case .ended:
|
|
||||||
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
|
|
||||||
switch gesture {
|
|
||||||
case .tap:
|
|
||||||
self.tapped?()
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layout() {
|
|
||||||
super.layout()
|
|
||||||
|
|
||||||
self.node.frame = self.bounds
|
|
||||||
self.shadowNode.frame = self.bounds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StackContainerNode: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDelegate {
|
|
||||||
private let scrollNode: ASScrollNode
|
|
||||||
private var nodes: [StackItemContainerNode]
|
|
||||||
|
|
||||||
private var deleteGestureRecognizer: UIPanGestureRecognizer?
|
|
||||||
private var offsetsForDeletingItems: [Int: CGPoint]?
|
|
||||||
private var currentDeletingIndexPath: Int?
|
|
||||||
private var deletingOffset: CGFloat?
|
|
||||||
|
|
||||||
private var animatingIn = false
|
|
||||||
|
|
||||||
private var validLayout: CGSize?
|
|
||||||
|
|
||||||
override public init() {
|
|
||||||
self.scrollNode = ASScrollNode()
|
|
||||||
self.nodes = []
|
|
||||||
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
self.backgroundColor = .black
|
|
||||||
|
|
||||||
self.addSubnode(self.scrollNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func didLoad() {
|
|
||||||
super.didLoad()
|
|
||||||
|
|
||||||
if #available(iOS 11.0, *) {
|
|
||||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
|
||||||
}
|
|
||||||
|
|
||||||
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
|
|
||||||
self.scrollNode.view.alwaysBounceVertical = true
|
|
||||||
|
|
||||||
let deleteGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPanToDelete(gestureRecognizer:)))
|
|
||||||
deleteGestureRecognizer.delegate = self.wrappedGestureRecognizerDelegate
|
|
||||||
deleteGestureRecognizer.delaysTouchesBegan = true
|
|
||||||
self.scrollNode.view.addGestureRecognizer(deleteGestureRecognizer)
|
|
||||||
self.deleteGestureRecognizer = deleteGestureRecognizer
|
|
||||||
}
|
|
||||||
|
|
||||||
func item(forYPosition y: CGFloat) -> Int? {
|
|
||||||
let itemCount = self.nodes.count
|
|
||||||
let bounds = self.scrollNode.bounds
|
|
||||||
|
|
||||||
let spacing = interitemSpacing(itemCount: itemCount, bounds: bounds)
|
|
||||||
return max(0, min(Int(floor((y - sectionInsetTop) / spacing)), itemCount - 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
||||||
guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let touch = panGesture.location(in: gestureRecognizer.view)
|
|
||||||
let velocity = panGesture.velocity(in: gestureRecognizer.view)
|
|
||||||
|
|
||||||
if abs(velocity.x) > abs(velocity.y), let item = self.item(forYPosition: touch.y) {
|
|
||||||
return item > 0
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func didPanToDelete(gestureRecognizer: UIPanGestureRecognizer) {
|
|
||||||
let scrollView = self.scrollNode.view
|
|
||||||
|
|
||||||
switch gestureRecognizer.state {
|
|
||||||
case .began:
|
|
||||||
let touch = gestureRecognizer.location(in: scrollView)
|
|
||||||
guard let item = self.item(forYPosition: touch.y) else { return }
|
|
||||||
|
|
||||||
self.currentDeletingIndexPath = item
|
|
||||||
case .changed:
|
|
||||||
guard let _ = self.currentDeletingIndexPath else { return }
|
|
||||||
|
|
||||||
var delta = gestureRecognizer.translation(in: scrollView)
|
|
||||||
delta.y = 0
|
|
||||||
|
|
||||||
if let offset = self.deletingOffset {
|
|
||||||
self.deletingOffset = offset + delta.x
|
|
||||||
} else {
|
|
||||||
self.deletingOffset = delta.x
|
|
||||||
}
|
|
||||||
|
|
||||||
gestureRecognizer.setTranslation(.zero, in: scrollView)
|
|
||||||
|
|
||||||
self.updateLayout()
|
|
||||||
case .ended:
|
|
||||||
if let _ = self.currentDeletingIndexPath {
|
|
||||||
if let offset = self.deletingOffset {
|
|
||||||
if offset < -self.frame.width / 2.0 {
|
|
||||||
self.deletingOffset = -self.frame.width
|
|
||||||
} else {
|
|
||||||
self.deletingOffset = nil
|
|
||||||
self.currentDeletingIndexPath = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.3) {
|
|
||||||
self.updateLayout()
|
|
||||||
}
|
|
||||||
case .cancelled, .failed:
|
|
||||||
self.currentDeletingIndexPath = nil
|
|
||||||
self.deletingOffset = nil
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setup() {
|
|
||||||
let images: [UIImage] = [UIImage(bundleImageName: "Settings/test1")!, UIImage(bundleImageName: "Settings/test5")!, UIImage(bundleImageName: "Settings/test4")!, UIImage(bundleImageName: "Settings/test3")!, UIImage(bundleImageName: "Settings/test2")!]
|
|
||||||
for i in 0 ..< 5 {
|
|
||||||
let node = ASImageNode()
|
|
||||||
node.image = images[i]
|
|
||||||
|
|
||||||
let containerNode = StackItemContainerNode(node: node)
|
|
||||||
containerNode.tapped = { [weak self] in
|
|
||||||
self?.animateIn(index: i)
|
|
||||||
}
|
|
||||||
containerNode.highlighted = { [weak self] highlighted in
|
|
||||||
self?.highlight(index: i, value: highlighted)
|
|
||||||
}
|
|
||||||
self.nodes.append(containerNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var index: Int = 0
|
|
||||||
let bounds = self.scrollNode.view.bounds
|
|
||||||
let itemCount = self.nodes.count
|
|
||||||
|
|
||||||
for node in self.nodes {
|
|
||||||
self.scrollNode.addSubnode(node)
|
|
||||||
|
|
||||||
let size = CGSize(width: self.frame.width, height: self.frame.height)
|
|
||||||
let frame = frameForIndex(index: index, size: size, itemCount: itemCount, bounds: bounds)
|
|
||||||
node.frame = frame
|
|
||||||
let transform = final3dTransform(for: frame.minY, size: frame.size, contentHeight: nil, itemCount: itemCount, bounds: bounds)
|
|
||||||
node.transform = transform
|
|
||||||
index += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if let lastFrame = self.nodes.last?.frame {
|
|
||||||
self.scrollNode.view.contentSize = CGSize(width: self.frame.width, height: lastFrame.minY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func animateIn(index: Int) {
|
|
||||||
let node = self.nodes[index]
|
|
||||||
|
|
||||||
self.animatingIn = true
|
|
||||||
self.scrollNode.view.isUserInteractionEnabled = false
|
|
||||||
node.animateIn()
|
|
||||||
UIView.animate(withDuration: 0.3) {
|
|
||||||
node.transform = CATransform3DIdentity
|
|
||||||
node.position = CGPoint(x: self.scrollNode.frame.width / 2.0, y: self.scrollNode.frame.height / 2.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0 ..< index {
|
|
||||||
let node = self.nodes[i]
|
|
||||||
node.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -550.0), duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, mediaTimingFunction: nil, removeOnCompletion: false, additive: true, force: false, completion: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in (index + 1) ..< self.nodes.count {
|
|
||||||
let node = self.nodes[i]
|
|
||||||
node.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 550.0), duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, mediaTimingFunction: nil, removeOnCompletion: false, additive: true, force: false, completion: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func highlight(index: Int, value: Bool) {
|
|
||||||
let node = self.nodes[index]
|
|
||||||
|
|
||||||
let bounds = self.scrollNode.view.bounds
|
|
||||||
let contentHeight = self.scrollNode.view.contentSize.height
|
|
||||||
let itemCount = self.nodes.count
|
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.4) {
|
|
||||||
let transform = final3dTransform(for: node.frame.minY, size: node.frame.size, contentHeight: contentHeight, itemCount: itemCount, additionalAngle: value ? 0.04 : nil, bounds: bounds)
|
|
||||||
node.transform = transform
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
||||||
guard !self.animatingIn else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.updateLayout()
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateLayout() {
|
|
||||||
let bounds = self.scrollNode.view.bounds
|
|
||||||
let contentHeight = self.scrollNode.view.contentSize.height
|
|
||||||
let itemCount = self.nodes.count
|
|
||||||
|
|
||||||
var index: Int = 0
|
|
||||||
for node in self.nodes {
|
|
||||||
let initialTransform = final3dTransform(for: node.frame.minY, size: node.frame.size, contentHeight: contentHeight, itemCount: itemCount, bounds: bounds)
|
|
||||||
let initialFrame = frameForIndex(index: index, size: node.frame.size, itemCount: itemCount, bounds: bounds)
|
|
||||||
|
|
||||||
var targetTransform: CATransform3D?
|
|
||||||
var targetPosition: CGPoint?
|
|
||||||
|
|
||||||
var finalPosition = initialFrame.center
|
|
||||||
|
|
||||||
if let deletingIndex = self.currentDeletingIndexPath, let offset = self.deletingOffset {
|
|
||||||
if deletingIndex == index {
|
|
||||||
finalPosition = CGPoint(x: self.frame.width / 2.0 + min(offset, 0.0), y: node.position.y)
|
|
||||||
} else if index < deletingIndex {
|
|
||||||
let frame = frameForIndex(index: index, size: node.frame.size, itemCount: itemCount - 1, bounds: bounds)
|
|
||||||
targetPosition = frame.center
|
|
||||||
|
|
||||||
let spacing = interitemSpacing(itemCount: itemCount - 1, bounds: bounds)
|
|
||||||
targetTransform = final3dTransform(for: frame.minY, size: node.frame.size, contentHeight: contentHeight - node.frame.height - spacing, itemCount: itemCount - 1, bounds: bounds)
|
|
||||||
} else {
|
|
||||||
let frame = frameForIndex(index: index - 1, size: node.frame.size, itemCount: itemCount - 1, bounds: bounds)
|
|
||||||
targetPosition = frame.center
|
|
||||||
|
|
||||||
let spacing = interitemSpacing(itemCount: itemCount - 1, bounds: bounds)
|
|
||||||
targetTransform = final3dTransform(for: frame.minY, size: node.frame.size, contentHeight: contentHeight - node.frame.height - spacing, itemCount: itemCount - 1, bounds: bounds)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
node.position = initialFrame.center
|
|
||||||
}
|
|
||||||
|
|
||||||
var finalTransform = initialTransform
|
|
||||||
if let targetTransform = targetTransform, let offset = self.deletingOffset {
|
|
||||||
let progress = min(1.0, abs(offset / (self.frame.width)))
|
|
||||||
finalTransform = initialTransform.interpolate(other: targetTransform, progress: progress)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let targetPosition = targetPosition, let offset = self.deletingOffset {
|
|
||||||
let progress = min(1.0, abs(offset / (self.frame.width)))
|
|
||||||
finalPosition = CGPoint(x: finalPosition.x + (targetPosition.x - finalPosition.x) * progress, y: finalPosition.y + (targetPosition.y - finalPosition.y) * progress)
|
|
||||||
}
|
|
||||||
|
|
||||||
node.transform = finalTransform
|
|
||||||
node.position = finalPosition
|
|
||||||
|
|
||||||
index += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func update(size: CGSize) {
|
|
||||||
let hadValidLayout = self.validLayout != nil
|
|
||||||
self.validLayout = size
|
|
||||||
|
|
||||||
self.scrollNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
||||||
|
|
||||||
if !hadValidLayout {
|
|
||||||
self.setup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1177,9 +1177,11 @@ public extension ContainedViewLayoutTransition {
|
|||||||
self.updateTransform(layer: node.layer, transform: transform, beginWithCurrentState: beginWithCurrentState, delay: delay, completion: completion)
|
self.updateTransform(layer: node.layer, transform: transform, beginWithCurrentState: beginWithCurrentState, delay: delay, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTransform(layer: CALayer, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
func updateTransform(node: ASDisplayNode, transform: CATransform3D, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||||
let transform = CATransform3DMakeAffineTransform(transform)
|
self.updateTransform(layer: node.layer, transform: transform, beginWithCurrentState: beginWithCurrentState, delay: delay, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateTransform(layer: CALayer, transform: CATransform3D, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||||
if CATransform3DEqualToTransform(layer.transform, transform) {
|
if CATransform3DEqualToTransform(layer.transform, transform) {
|
||||||
if let completion = completion {
|
if let completion = completion {
|
||||||
completion(true)
|
completion(true)
|
||||||
@ -1206,6 +1208,11 @@ public extension ContainedViewLayoutTransition {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateTransform(layer: CALayer, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
let transform = CATransform3DMakeAffineTransform(transform)
|
||||||
|
self.updateTransform(layer: layer, transform: transform, beginWithCurrentState: beginWithCurrentState, delay: delay, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
func updateTransformScale(node: ASDisplayNode, scale: CGFloat, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
func updateTransformScale(node: ASDisplayNode, scale: CGFloat, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||||
let t = node.layer.transform
|
let t = node.layer.transform
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import Foundation
|
||||||
|
import AsyncDisplayKit
|
||||||
|
|
||||||
|
public protocol MinimizedContainer: ASDisplayNode {
|
||||||
|
var willMaximize: (() -> Void)? { get set }
|
||||||
|
|
||||||
|
func addController(_ viewController: ViewController, transition: ContainedViewLayoutTransition)
|
||||||
|
func maximizeController(_ viewController: ViewController, animated: Bool, completion: @escaping (Bool) -> Void)
|
||||||
|
func dismissAll(completion: @escaping () -> Void)
|
||||||
|
|
||||||
|
func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition)
|
||||||
|
func collapsedHeight(layout: ContainerViewLayout) -> CGFloat
|
||||||
|
}
|
@ -150,6 +150,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
private var rootModalFrame: NavigationModalFrame?
|
private var rootModalFrame: NavigationModalFrame?
|
||||||
private var modalContainers: [NavigationModalContainer] = []
|
private var modalContainers: [NavigationModalContainer] = []
|
||||||
private var overlayContainers: [NavigationOverlayContainer] = []
|
private var overlayContainers: [NavigationOverlayContainer] = []
|
||||||
|
private var minimizedContainer: MinimizedContainer?
|
||||||
|
|
||||||
private var globalOverlayContainers: [NavigationOverlayContainer] = []
|
private var globalOverlayContainers: [NavigationOverlayContainer] = []
|
||||||
private var globalOverlayBelowKeyboardContainerParent: GlobalOverlayContainerParent?
|
private var globalOverlayBelowKeyboardContainerParent: GlobalOverlayContainerParent?
|
||||||
@ -180,15 +181,6 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var _minimizedViewControllers: [ViewController] = []
|
|
||||||
open var minimizedViewControllers: [UIViewController] {
|
|
||||||
get {
|
|
||||||
return self._minimizedViewControllers.map { $0 as UIViewController }
|
|
||||||
} set(value) {
|
|
||||||
self.setMinimizedViewControllers(value, animated: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var _viewControllersPromise = ValuePromise<[UIViewController]>()
|
private var _viewControllersPromise = ValuePromise<[UIViewController]>()
|
||||||
public var viewControllersSignal: Signal<[UIViewController], NoError> {
|
public var viewControllersSignal: Signal<[UIViewController], NoError> {
|
||||||
return _viewControllersPromise.get()
|
return _viewControllersPromise.get()
|
||||||
@ -475,7 +467,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
transition.updateFrame(node: globalOverlayContainerParent, frame: CGRect(origin: CGPoint(), size: layout.size))
|
transition.updateFrame(node: globalOverlayContainerParent, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||||
}
|
}
|
||||||
|
|
||||||
let navigationLayout = makeNavigationLayout(mode: self.mode, layout: layout, controllers: self._viewControllers, minimizedControllers: self._minimizedViewControllers)
|
let navigationLayout = makeNavigationLayout(mode: self.mode, layout: layout, controllers: self._viewControllers)
|
||||||
|
|
||||||
var transition = transition
|
var transition = transition
|
||||||
var statusBarStyle: StatusBarStyle = .Ignore
|
var statusBarStyle: StatusBarStyle = .Ignore
|
||||||
@ -497,9 +489,8 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
let modalContainer: NavigationModalContainer
|
let modalContainer: NavigationModalContainer
|
||||||
if let existingModalContainer = existingModalContainer {
|
if let existingModalContainer = existingModalContainer {
|
||||||
modalContainer = existingModalContainer
|
modalContainer = existingModalContainer
|
||||||
modalContainer.isMinimized = navigationLayout.modal[i].isMinimized
|
|
||||||
} else {
|
} else {
|
||||||
modalContainer = NavigationModalContainer(theme: self.theme, isFlat: navigationLayout.modal[i].isFlat, isMinimized: navigationLayout.modal[i].isMinimized, controllerRemoved: { [weak self] controller in
|
modalContainer = NavigationModalContainer(theme: self.theme, isFlat: navigationLayout.modal[i].isFlat, controllerRemoved: { [weak self] controller in
|
||||||
self?.controllerRemoved(controller)
|
self?.controllerRemoved(controller)
|
||||||
})
|
})
|
||||||
modalContainer.container.statusBarStyleUpdated = { [weak self] transition in
|
modalContainer.container.statusBarStyleUpdated = { [weak self] transition in
|
||||||
@ -534,32 +525,6 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
strongSelf.setViewControllers(controllers, animated: false)
|
strongSelf.setViewControllers(controllers, animated: false)
|
||||||
strongSelf.ignoreInputHeight = false
|
strongSelf.ignoreInputHeight = false
|
||||||
}
|
}
|
||||||
modalContainer.minimizedRequestMaximize = { [weak self] in
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var controllers = self._viewControllers
|
|
||||||
for controller in self._minimizedViewControllers {
|
|
||||||
controllers.append(controller)
|
|
||||||
}
|
|
||||||
self._viewControllers = controllers
|
|
||||||
self._minimizedViewControllers = []
|
|
||||||
|
|
||||||
self.updateContainersNonReentrant(transition: .animated(duration: 0.5, curve: .spring))
|
|
||||||
}
|
|
||||||
modalContainer.minimizedRequestDismiss = { [weak self, weak modalContainer] animated in
|
|
||||||
guard let self, let modalContainer else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let minimizedControllers = self.minimizedViewControllers.filter { controller in
|
|
||||||
return !modalContainer.container.controllers.contains(where: { $0 === controller })
|
|
||||||
}
|
|
||||||
if minimizedControllers.count != self.minimizedViewControllers.count {
|
|
||||||
self.setMinimizedViewControllers(minimizedControllers, animated: animated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
modalContainers.append(modalContainer)
|
modalContainers.append(modalContainer)
|
||||||
}
|
}
|
||||||
@ -731,7 +696,6 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
var topVisibleModalContainerWithStatusBar: NavigationModalContainer?
|
var topVisibleModalContainerWithStatusBar: NavigationModalContainer?
|
||||||
var visibleModalCount = 0
|
var visibleModalCount = 0
|
||||||
var topModalIsFlat = false
|
var topModalIsFlat = false
|
||||||
var topModalIsMinimized = false
|
|
||||||
var topFlatModalHasProgress = false
|
var topFlatModalHasProgress = false
|
||||||
let isLandscape = layout.orientation == .landscape
|
let isLandscape = layout.orientation == .landscape
|
||||||
var hasVisibleStandaloneModal = false
|
var hasVisibleStandaloneModal = false
|
||||||
@ -771,7 +735,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
modalStyleOverlayTransitionFactor = max(modalStyleOverlayTransitionFactor, lastController.modalStyleOverlayTransitionFactor)
|
modalStyleOverlayTransitionFactor = max(modalStyleOverlayTransitionFactor, lastController.modalStyleOverlayTransitionFactor)
|
||||||
topFlatModalHasProgress = modalStyleOverlayTransitionFactor > 0.0
|
topFlatModalHasProgress = modalStyleOverlayTransitionFactor > 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
containerTransition.updateFrame(node: modalContainer, frame: CGRect(origin: CGPoint(), size: layout.size))
|
containerTransition.updateFrame(node: modalContainer, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||||
modalContainer.update(layout: modalContainer.isFlat ? globalOverlayLayout : layout, controllers: navigationLayout.modal[i].controllers, coveredByModalTransition: effectiveModalTransition, transition: containerTransition)
|
modalContainer.update(layout: modalContainer.isFlat ? globalOverlayLayout : layout, controllers: navigationLayout.modal[i].controllers, coveredByModalTransition: effectiveModalTransition, transition: containerTransition)
|
||||||
|
|
||||||
@ -791,16 +755,13 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if modalContainer.supernode != nil {
|
if modalContainer.supernode != nil {
|
||||||
if !hasVisibleStandaloneModal && !isStandaloneModal && !modalContainer.isFlat && !modalContainer.isMinimized {
|
if !hasVisibleStandaloneModal && !isStandaloneModal && !modalContainer.isFlat {
|
||||||
visibleModalCount += 1
|
visibleModalCount += 1
|
||||||
}
|
}
|
||||||
if isStandaloneModal {
|
if isStandaloneModal {
|
||||||
hasVisibleStandaloneModal = true
|
hasVisibleStandaloneModal = true
|
||||||
visibleModalCount = 0
|
visibleModalCount = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
topModalIsMinimized = modalContainer.isMinimized
|
|
||||||
|
|
||||||
if previousModalContainer == nil {
|
if previousModalContainer == nil {
|
||||||
topModalIsFlat = modalContainer.isFlat
|
topModalIsFlat = modalContainer.isFlat
|
||||||
|
|
||||||
@ -857,6 +818,11 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.isMaximizing && layout.size.width < layout.size.height {
|
||||||
|
modalStyleOverlayTransitionFactor = 1.0
|
||||||
|
topFlatModalHasProgress = true
|
||||||
|
}
|
||||||
|
|
||||||
layout.additionalInsets.left = max(layout.intrinsicInsets.left, additionalSideInsets.left)
|
layout.additionalInsets.left = max(layout.intrinsicInsets.left, additionalSideInsets.left)
|
||||||
layout.additionalInsets.right = max(layout.intrinsicInsets.right, additionalSideInsets.right)
|
layout.additionalInsets.right = max(layout.intrinsicInsets.right, additionalSideInsets.right)
|
||||||
|
|
||||||
@ -865,18 +831,18 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
if let rootContainer = self.rootContainer {
|
if let rootContainer = self.rootContainer {
|
||||||
switch rootContainer {
|
switch rootContainer {
|
||||||
case let .flat(flatContainer):
|
case let .flat(flatContainer):
|
||||||
if let previousModalContainer, !previousModalContainer.isMinimized {
|
if previousModalContainer == nil {
|
||||||
flatContainer.keyboardViewManager = nil
|
|
||||||
flatContainer.canHaveKeyboardFocus = false
|
|
||||||
} else {
|
|
||||||
flatContainer.keyboardViewManager = self.keyboardViewManager
|
flatContainer.keyboardViewManager = self.keyboardViewManager
|
||||||
flatContainer.canHaveKeyboardFocus = true
|
flatContainer.canHaveKeyboardFocus = true
|
||||||
|
} else {
|
||||||
|
flatContainer.keyboardViewManager = nil
|
||||||
|
flatContainer.canHaveKeyboardFocus = false
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedSize = layout.size
|
var updatedSize = layout.size
|
||||||
var updatedIntrinsicInsets = layout.intrinsicInsets
|
var updatedIntrinsicInsets = layout.intrinsicInsets
|
||||||
if topModalIsMinimized && (layout.inputHeight ?? 0.0).isZero {
|
if let minimizedContainer = self.minimizedContainer, (layout.inputHeight ?? 0.0).isZero {
|
||||||
updatedSize.height -= 81.0
|
updatedSize.height -= minimizedContainer.collapsedHeight(layout: layout)
|
||||||
updatedIntrinsicInsets.bottom = 0.0
|
updatedIntrinsicInsets.bottom = 0.0
|
||||||
}
|
}
|
||||||
let updatedLayout = layout.withUpdatedSize(updatedSize).withUpdatedIntrinsicInsets(updatedIntrinsicInsets)
|
let updatedLayout = layout.withUpdatedSize(updatedSize).withUpdatedIntrinsicInsets(updatedIntrinsicInsets)
|
||||||
@ -1145,6 +1111,11 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let minimizedContainer = self.minimizedContainer {
|
||||||
|
minimizedContainer.frame = CGRect(origin: .zero, size: layout.size)
|
||||||
|
minimizedContainer.updateLayout(layout, transition: transition)
|
||||||
|
}
|
||||||
|
|
||||||
if self.inCallStatusBar != nil {
|
if self.inCallStatusBar != nil {
|
||||||
statusBarStyle = .White
|
statusBarStyle = .White
|
||||||
}
|
}
|
||||||
@ -1432,11 +1403,6 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
self.setViewControllers(controllers, animated: animated)
|
self.setViewControllers(controllers, animated: animated)
|
||||||
self.ignoreInputHeight = false
|
self.ignoreInputHeight = false
|
||||||
}
|
}
|
||||||
|
|
||||||
let minimizedControllers = self.minimizedViewControllers.filter({ $0 !== controller })
|
|
||||||
if minimizedControllers.count != self.minimizedViewControllers.count {
|
|
||||||
self.setMinimizedViewControllers(minimizedControllers, animated: animated)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func replaceController(_ controller: ViewController, with other: ViewController, animated: Bool) {
|
public func replaceController(_ controller: ViewController, with other: ViewController, animated: Bool) {
|
||||||
@ -1575,28 +1541,71 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
}
|
}
|
||||||
self._viewControllersPromise.set(self.viewControllers)
|
self._viewControllersPromise.set(self.viewControllers)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setMinimizedViewControllers(_ viewControllers: [UIViewController], animated: Bool) {
|
|
||||||
self._viewControllers = self._viewControllers.filter { controller in
|
|
||||||
return !viewControllers.contains(controller)
|
|
||||||
}
|
|
||||||
|
|
||||||
self._minimizedViewControllers = viewControllers.map { controller in
|
public func minimizeViewController(_ viewController: ViewController, damping: CGFloat?, velocity: CGFloat? = nil, setupContainer: (MinimizedContainer?) -> MinimizedContainer?, animated: Bool) {
|
||||||
let controller = controller as! ViewController
|
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .customSpring(damping: damping ?? 124.0, initialVelocity: velocity ?? 0.0)) : .immediate
|
||||||
controller.navigation_setNavigationController(self)
|
|
||||||
return controller
|
let minimizedContainer = setupContainer(self.minimizedContainer)
|
||||||
}
|
if self.minimizedContainer !== minimizedContainer {
|
||||||
if let layout = self.validLayout {
|
minimizedContainer?.willMaximize = { [weak self] in
|
||||||
self.updateContainers(layout: layout, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate, completion: { [weak self] in
|
guard let self else {
|
||||||
self?.notifyAccessibilityScreenChanged()
|
return
|
||||||
})
|
}
|
||||||
|
self.isMaximizing = true
|
||||||
|
self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring))
|
||||||
|
}
|
||||||
|
self.minimizedContainer?.removeFromSupernode()
|
||||||
|
self.minimizedContainer = minimizedContainer
|
||||||
|
|
||||||
|
if let minimizedContainer {
|
||||||
|
if let modalContainer = self.modalContainers.first {
|
||||||
|
self.displayNode.insertSubnode(minimizedContainer, belowSubnode: modalContainer)
|
||||||
|
} else {
|
||||||
|
self.displayNode.addSubnode(minimizedContainer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.updateContainersNonReentrant(transition: transition)
|
||||||
}
|
}
|
||||||
|
self.filterController(viewController, animated: true)
|
||||||
|
minimizedContainer?.addController(viewController, transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func minimizeViewController(_ viewController: UIViewController, animated: Bool) {
|
private var isMaximizing = false
|
||||||
var controllers = self.minimizedViewControllers
|
public func maximizeViewController(_ viewController: ViewController, animated: Bool) {
|
||||||
controllers.append(viewController)
|
guard let minimizedContainer = self.minimizedContainer else {
|
||||||
self.setMinimizedViewControllers(controllers, animated: animated)
|
return
|
||||||
|
}
|
||||||
|
if animated {
|
||||||
|
self.isMaximizing = true
|
||||||
|
self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring))
|
||||||
|
}
|
||||||
|
minimizedContainer.maximizeController(viewController, animated: animated, completion: { [weak self] dismissed in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var viewControllers = self.viewControllers
|
||||||
|
viewControllers.append(viewController)
|
||||||
|
self.setViewControllers(viewControllers, animated: false)
|
||||||
|
self.isMaximizing = false
|
||||||
|
|
||||||
|
if dismissed, let minimizedContainer = self.minimizedContainer {
|
||||||
|
self.minimizedContainer = nil
|
||||||
|
minimizedContainer.removeFromSupernode()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public func dismissMinimizedControllers(animated: Bool) {
|
||||||
|
guard let minimizedContainer = self.minimizedContainer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.minimizedContainer = nil
|
||||||
|
|
||||||
|
minimizedContainer.dismissAll(completion: { [weak minimizedContainer] in
|
||||||
|
minimizedContainer?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
self.updateContainersNonReentrant(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var _keepModalDismissProgress = false
|
public var _keepModalDismissProgress = false
|
||||||
@ -1862,8 +1871,9 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
transition.updateTransform(node: container, transform: CGAffineTransformMakeTranslation(offset, 0.0))
|
transition.updateTransform(node: container, transform: CGAffineTransformMakeTranslation(offset, 0.0))
|
||||||
if let minimizedModalContainer = self.modalContainers.first(where: { $0.isMinimized }) {
|
|
||||||
transition.updateTransform(node: minimizedModalContainer, transform: CGAffineTransformMakeTranslation(offset, 0.0))
|
if let minimizedContainer = self.minimizedContainer {
|
||||||
|
transition.updateTransform(node: minimizedContainer, transform: CGAffineTransformMakeTranslation(offset, 0.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,6 @@ struct ModalContainerLayout {
|
|||||||
var controllers: [ViewController]
|
var controllers: [ViewController]
|
||||||
var isFlat: Bool
|
var isFlat: Bool
|
||||||
var isStandalone: Bool
|
var isStandalone: Bool
|
||||||
var isMinimized: Bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NavigationLayout {
|
struct NavigationLayout {
|
||||||
@ -20,7 +19,7 @@ struct NavigationLayout {
|
|||||||
var modal: [ModalContainerLayout]
|
var modal: [ModalContainerLayout]
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewLayout, controllers: [ViewController], minimizedControllers: [ViewController]) -> NavigationLayout {
|
func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewLayout, controllers: [ViewController]) -> NavigationLayout {
|
||||||
var rootControllers: [ViewController] = []
|
var rootControllers: [ViewController] = []
|
||||||
var modalStack: [ModalContainerLayout] = []
|
var modalStack: [ModalContainerLayout] = []
|
||||||
for controller in controllers {
|
for controller in controllers {
|
||||||
@ -55,7 +54,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL
|
|||||||
if requiresModal {
|
if requiresModal {
|
||||||
controller._presentedInModal = true
|
controller._presentedInModal = true
|
||||||
if beginsModal || modalStack.isEmpty || modalStack[modalStack.count - 1].isStandalone {
|
if beginsModal || modalStack.isEmpty || modalStack[modalStack.count - 1].isStandalone {
|
||||||
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone, isMinimized: false))
|
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone))
|
||||||
} else {
|
} else {
|
||||||
modalStack[modalStack.count - 1].controllers.append(controller)
|
modalStack[modalStack.count - 1].controllers.append(controller)
|
||||||
}
|
}
|
||||||
@ -65,7 +64,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL
|
|||||||
controller._presentedInModal = true
|
controller._presentedInModal = true
|
||||||
}
|
}
|
||||||
if modalStack[modalStack.count - 1].isStandalone {
|
if modalStack[modalStack.count - 1].isStandalone {
|
||||||
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone, isMinimized: false))
|
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone))
|
||||||
} else {
|
} else {
|
||||||
modalStack[modalStack.count - 1].controllers.append(controller)
|
modalStack[modalStack.count - 1].controllers.append(controller)
|
||||||
}
|
}
|
||||||
@ -75,22 +74,6 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var minimizedModalContainer: ModalContainerLayout?
|
|
||||||
for controller in minimizedControllers {
|
|
||||||
controller._presentedInModal = false
|
|
||||||
if var container = minimizedModalContainer {
|
|
||||||
container.controllers.append(controller)
|
|
||||||
minimizedModalContainer = container
|
|
||||||
} else {
|
|
||||||
let container = ModalContainerLayout(controllers: [controller], isFlat: false, isStandalone: false, isMinimized: true)
|
|
||||||
minimizedModalContainer = container
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let minimizedModalContainer {
|
|
||||||
modalStack.insert(minimizedModalContainer, at: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
let rootLayout: RootNavigationLayout
|
let rootLayout: RootNavigationLayout
|
||||||
switch mode {
|
switch mode {
|
||||||
case .single:
|
case .single:
|
||||||
|
@ -4,36 +4,14 @@ import AsyncDisplayKit
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import UIKitRuntimeUtils
|
import UIKitRuntimeUtils
|
||||||
|
|
||||||
private let minimizedMask: UIImage? = {
|
|
||||||
return generateImage(CGSize(width: 22.0, height: 24.0), rotatedContext: { size, context in
|
|
||||||
context.setFillColor(UIColor.black.cgColor)
|
|
||||||
context.fill(CGRect(origin: .zero, size: size))
|
|
||||||
|
|
||||||
context.setBlendMode(.clear)
|
|
||||||
context.setFillColor(UIColor.clear.cgColor)
|
|
||||||
|
|
||||||
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: -10, width: 22, height: 20), cornerRadius: 10)
|
|
||||||
context.addPath(path.cgPath)
|
|
||||||
context.fillPath()
|
|
||||||
})?.stretchableImage(withLeftCapWidth: 11, topCapHeight: 12)
|
|
||||||
}()
|
|
||||||
|
|
||||||
final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDelegate {
|
final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDelegate {
|
||||||
private var theme: NavigationControllerTheme
|
private var theme: NavigationControllerTheme
|
||||||
let isFlat: Bool
|
let isFlat: Bool
|
||||||
var isMinimized: Bool
|
|
||||||
var appliedIsMinimized: Bool = false
|
|
||||||
|
|
||||||
private let minimizedFrameNode: ASImageNode
|
|
||||||
private let dim: ASDisplayNode
|
private let dim: ASDisplayNode
|
||||||
private let scrollNode: ASScrollNode
|
private let scrollNode: ASScrollNode
|
||||||
let container: NavigationContainer
|
let container: NavigationContainer
|
||||||
|
|
||||||
private let minimizedBackgroundNode: ASDisplayNode
|
|
||||||
private let minimizedTitleNode: ImmediateTextNode
|
|
||||||
private let minimizedCloseButton: HighlightableButtonNode
|
|
||||||
private var minimizedTitleDisposable: Disposable?
|
|
||||||
|
|
||||||
private var panRecognizer: InteractiveTransitionGestureRecognizer?
|
private var panRecognizer: InteractiveTransitionGestureRecognizer?
|
||||||
|
|
||||||
private(set) var isReady: Bool = false
|
private(set) var isReady: Bool = false
|
||||||
@ -42,9 +20,6 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||||
var interactivelyDismissed: ((Bool) -> Void)?
|
var interactivelyDismissed: ((Bool) -> Void)?
|
||||||
|
|
||||||
var minimizedRequestDismiss: ((Bool) -> Void)?
|
|
||||||
var minimizedRequestMaximize: (() -> Void)?
|
|
||||||
|
|
||||||
private var isUpdatingState = false
|
private var isUpdatingState = false
|
||||||
private var ignoreScrolling = false
|
private var ignoreScrolling = false
|
||||||
private var isDismissed = false
|
private var isDismissed = false
|
||||||
@ -67,14 +42,9 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(theme: NavigationControllerTheme, isFlat: Bool, isMinimized: Bool, controllerRemoved: @escaping (ViewController) -> Void) {
|
init(theme: NavigationControllerTheme, isFlat: Bool, controllerRemoved: @escaping (ViewController) -> Void) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.isFlat = isFlat
|
self.isFlat = isFlat
|
||||||
self.isMinimized = isMinimized
|
|
||||||
|
|
||||||
self.minimizedFrameNode = ASImageNode()
|
|
||||||
self.minimizedFrameNode.contentMode = .scaleToFill
|
|
||||||
self.minimizedFrameNode.image = minimizedMask
|
|
||||||
|
|
||||||
self.dim = ASDisplayNode()
|
self.dim = ASDisplayNode()
|
||||||
self.dim.alpha = 0.0
|
self.dim.alpha = 0.0
|
||||||
@ -84,28 +54,12 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
self.container = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved)
|
self.container = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved)
|
||||||
self.container.clipsToBounds = true
|
self.container.clipsToBounds = true
|
||||||
|
|
||||||
self.minimizedBackgroundNode = ASDisplayNode()
|
|
||||||
self.minimizedBackgroundNode.clipsToBounds = true
|
|
||||||
self.minimizedBackgroundNode.backgroundColor = theme.navigationBar.opaqueBackgroundColor
|
|
||||||
|
|
||||||
self.minimizedTitleNode = ImmediateTextNode()
|
|
||||||
|
|
||||||
self.minimizedCloseButton = HighlightableButtonNode()
|
|
||||||
self.minimizedCloseButton.setImage(UIImage(bundleImageName: "Instant View/Close"), for: .normal)
|
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.addSubnode(self.minimizedFrameNode)
|
|
||||||
self.addSubnode(self.dim)
|
self.addSubnode(self.dim)
|
||||||
self.addSubnode(self.scrollNode)
|
self.addSubnode(self.scrollNode)
|
||||||
self.scrollNode.addSubnode(self.container)
|
self.scrollNode.addSubnode(self.container)
|
||||||
|
|
||||||
self.addSubnode(self.minimizedBackgroundNode)
|
|
||||||
self.minimizedBackgroundNode.addSubnode(self.minimizedTitleNode)
|
|
||||||
self.minimizedBackgroundNode.addSubnode(self.minimizedCloseButton)
|
|
||||||
|
|
||||||
self.minimizedCloseButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside)
|
|
||||||
|
|
||||||
self.isReady = self.container.isReady
|
self.isReady = self.container.isReady
|
||||||
self.container.isReadyUpdated = { [weak self] in
|
self.container.isReadyUpdated = { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -120,11 +74,6 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
}
|
}
|
||||||
|
|
||||||
applySmoothRoundedCorners(self.container.layer)
|
applySmoothRoundedCorners(self.container.layer)
|
||||||
applySmoothRoundedCorners(self.minimizedBackgroundNode.layer)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
self.minimizedTitleDisposable?.dispose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didLoad() {
|
override func didLoad() {
|
||||||
@ -167,34 +116,26 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
self.view.addGestureRecognizer(panRecognizer)
|
self.view.addGestureRecognizer(panRecognizer)
|
||||||
self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
||||||
}
|
}
|
||||||
|
|
||||||
self.minimizedBackgroundNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.maximizeTapGesture(_:))))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
if gestureRecognizer.view === self.minimizedBackgroundNode.view {
|
if gestureRecognizer == self.panRecognizer, let gestureRecognizer = self.panRecognizer, gestureRecognizer.numberOfTouches == 0 {
|
||||||
return self.isMinimized
|
let translation = gestureRecognizer.velocity(in: gestureRecognizer.view)
|
||||||
} else if !self.isMinimized {
|
if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
|
||||||
if gestureRecognizer == self.panRecognizer, let gestureRecognizer = self.panRecognizer, gestureRecognizer.numberOfTouches == 0 {
|
return false
|
||||||
let translation = gestureRecognizer.velocity(in: gestureRecognizer.view)
|
|
||||||
if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if translation.x < 4.0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if self.isDismissed {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
if translation.x < 4.0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if self.isDismissed {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
} else {
|
} else {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -384,8 +325,6 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
|
|
||||||
self.validLayout = layout
|
self.validLayout = layout
|
||||||
|
|
||||||
let lastControllerUpdated = self.container.controllers.last !== controllers.last
|
|
||||||
|
|
||||||
var isStandaloneModal = false
|
var isStandaloneModal = false
|
||||||
if let controller = controllers.first, case .standaloneModal = controller.navigationPresentation {
|
if let controller = controllers.first, case .standaloneModal = controller.navigationPresentation {
|
||||||
isStandaloneModal = true
|
isStandaloneModal = true
|
||||||
@ -418,7 +357,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
if layout.metrics.widthClass == .compact || self.isFlat {
|
if layout.metrics.widthClass == .compact || self.isFlat {
|
||||||
self.panRecognizer?.isEnabled = true
|
self.panRecognizer?.isEnabled = true
|
||||||
self.container.clipsToBounds = true
|
self.container.clipsToBounds = true
|
||||||
if self.isFlat || self.isMinimized {
|
if self.isFlat {
|
||||||
self.dim.backgroundColor = .clear
|
self.dim.backgroundColor = .clear
|
||||||
} else {
|
} else {
|
||||||
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
||||||
@ -427,7 +366,6 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
self.container.cornerRadius = 0.0
|
self.container.cornerRadius = 0.0
|
||||||
} else {
|
} else {
|
||||||
self.container.cornerRadius = 10.0
|
self.container.cornerRadius = 10.0
|
||||||
self.minimizedBackgroundNode.cornerRadius = self.container.cornerRadius
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if #available(iOS 11.0, *) {
|
if #available(iOS 11.0, *) {
|
||||||
@ -436,8 +374,6 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
} else {
|
} else {
|
||||||
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||||
}
|
}
|
||||||
|
|
||||||
self.minimizedBackgroundNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var topInset: CGFloat
|
var topInset: CGFloat
|
||||||
@ -450,18 +386,10 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
containerFrame = unscaledFrame
|
containerFrame = unscaledFrame
|
||||||
} else {
|
} else {
|
||||||
topInset = 10.0
|
topInset = 10.0
|
||||||
|
if self.isFlat {
|
||||||
let height: CGFloat
|
topInset = 0.0
|
||||||
if self.isMinimized {
|
} else if let statusBarHeight = layout.statusBarHeight {
|
||||||
height = layout.size.height - topInset
|
topInset += statusBarHeight
|
||||||
topInset = layout.size.height - 78.0
|
|
||||||
} else {
|
|
||||||
if self.isFlat {
|
|
||||||
topInset = 0.0
|
|
||||||
} else if let statusBarHeight = layout.statusBarHeight {
|
|
||||||
topInset += statusBarHeight
|
|
||||||
}
|
|
||||||
height = layout.size.height - topInset
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let effectiveStatusBarHeight: CGFloat?
|
let effectiveStatusBarHeight: CGFloat?
|
||||||
@ -471,7 +399,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
effectiveStatusBarHeight = nil
|
effectiveStatusBarHeight = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width, height: height), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), safeInsets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.safeInsets.bottom, right: layout.safeInsets.right), additionalInsets: layout.additionalInsets, statusBarHeight: effectiveStatusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
|
containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width, height: layout.size.height - topInset), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), safeInsets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.safeInsets.bottom, right: layout.safeInsets.right), additionalInsets: layout.additionalInsets, statusBarHeight: effectiveStatusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
|
||||||
let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - coveredByModalTransition * 10.0), size: containerLayout.size)
|
let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - coveredByModalTransition * 10.0), size: containerLayout.size)
|
||||||
let maxScale: CGFloat = (containerLayout.size.width - 16.0 * 2.0) / containerLayout.size.width
|
let maxScale: CGFloat = (containerLayout.size.width - 16.0 * 2.0) / containerLayout.size.width
|
||||||
containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition
|
containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition
|
||||||
@ -479,59 +407,6 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
let scaledTopInset: CGFloat = topInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition
|
let scaledTopInset: CGFloat = topInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition
|
||||||
containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
|
containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
for controller in controllers {
|
|
||||||
controller.isMinimized = self.isMinimized
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.isMinimized != self.appliedIsMinimized {
|
|
||||||
self.appliedIsMinimized = self.isMinimized
|
|
||||||
|
|
||||||
if self.isMinimized {
|
|
||||||
let modalTopEdgeOffset = (controllers.last?.modalTopEdgeOffset ?? 0.0) + 96.0
|
|
||||||
if transition.isAnimated {
|
|
||||||
self.minimizedBackgroundNode.position = self.minimizedBackgroundNode.position.offsetBy(dx: 0.0, dy: modalTopEdgeOffset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transition.updateFrame(node: self.minimizedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: containerFrame.minY), size: CGSize(width: layout.size.width, height: 243.0)))
|
|
||||||
transition.updateAlpha(node: self.minimizedBackgroundNode, alpha: self.isMinimized ? 1.0 : 0.0)
|
|
||||||
self.minimizedBackgroundNode.cornerRadius = 10.0
|
|
||||||
self.minimizedBackgroundNode.isUserInteractionEnabled = self.isMinimized
|
|
||||||
|
|
||||||
let titleSideInset: CGFloat = 56.0
|
|
||||||
if self.isMinimized, let controller = controllers.last {
|
|
||||||
if lastControllerUpdated || self.minimizedTitleDisposable == nil {
|
|
||||||
var isFirstUpdate = true
|
|
||||||
self.minimizedTitleDisposable = (controller.titleSignal
|
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] title in
|
|
||||||
guard let self, let layout = self.validLayout else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.minimizedTitleNode.attributedText = NSAttributedString(string: title ?? "", font: Font.bold(17.0), textColor: self.theme.navigationBar.primaryTextColor)
|
|
||||||
|
|
||||||
if !isFirstUpdate {
|
|
||||||
let titleSize = self.minimizedTitleNode.updateLayout(CGSize(width: layout.size.width - titleSideInset * 2.0, height: 56.0))
|
|
||||||
self.minimizedTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - titleSize.width) / 2.0), y: 18.0), size: titleSize)
|
|
||||||
} else {
|
|
||||||
isFirstUpdate = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.minimizedTitleDisposable?.dispose()
|
|
||||||
self.minimizedTitleDisposable = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let titleSize = self.minimizedTitleNode.updateLayout(CGSize(width: layout.size.width - titleSideInset * 2.0, height: 56.0))
|
|
||||||
transition.updateFrame(node: self.minimizedTitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - titleSize.width) / 2.0), y: 18.0), size: titleSize))
|
|
||||||
|
|
||||||
transition.updateFrame(node: self.minimizedCloseButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: 46.0, height: 52.0)))
|
|
||||||
|
|
||||||
transition.updateAlpha(node: self.minimizedFrameNode, alpha: self.isMinimized ? 1.0 : 0.0)
|
|
||||||
transition.updateFrame(node: self.minimizedFrameNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - 81.0 - 10.0), size: CGSize(width: layout.size.width, height: 24.0 + 81.0)))
|
|
||||||
} else {
|
} else {
|
||||||
self.panRecognizer?.isEnabled = false
|
self.panRecognizer?.isEnabled = false
|
||||||
if self.isFlat {
|
if self.isFlat {
|
||||||
@ -610,17 +485,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||||
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||||
alphaTransition.updateAlpha(node: self.dim, alpha: 0.0, beginWithCurrentState: true)
|
alphaTransition.updateAlpha(node: self.dim, alpha: 0.0, beginWithCurrentState: true)
|
||||||
|
positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: self.bounds.height + self.container.bounds.height / 2.0 + self.bounds.height), beginWithCurrentState: true, completion: { [weak self] _ in
|
||||||
let targetY: CGFloat
|
|
||||||
if self.isMinimized {
|
|
||||||
let offset: CGFloat = 81.0 + 15.0
|
|
||||||
targetY = self.container.position.y + offset
|
|
||||||
positionTransition.updatePosition(node: self.minimizedBackgroundNode, position: CGPoint(x: self.minimizedBackgroundNode.position.x, y: self.minimizedBackgroundNode.position.y + offset))
|
|
||||||
} else {
|
|
||||||
targetY = self.bounds.height + self.container.bounds.height / 2.0 + self.bounds.height
|
|
||||||
}
|
|
||||||
|
|
||||||
positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: targetY), beginWithCurrentState: true, completion: { [weak self] _ in
|
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -648,14 +513,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !self.container.bounds.contains(self.view.convert(point, to: self.container.view)) {
|
if !self.container.bounds.contains(self.view.convert(point, to: self.container.view)) {
|
||||||
if self.isMinimized {
|
return self.dim.view
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return self.dim.view
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.isMinimized && result == self.minimizedBackgroundNode.view {
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
if self.isFlat {
|
if self.isFlat {
|
||||||
return result
|
return result
|
||||||
@ -710,22 +568,4 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||||||
self.scrollNode.view.isScrollEnabled = enableScrolling && !self.isFlat
|
self.scrollNode.view.isScrollEnabled = enableScrolling && !self.isFlat
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func closePressed() {
|
|
||||||
if !self.isDismissed {
|
|
||||||
self.minimizedRequestDismiss?(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func maximizeTapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
||||||
if case .ended = recognizer.state {
|
|
||||||
if !self.isDismissed {
|
|
||||||
if self.container.controllers.count == 1 {
|
|
||||||
self.minimizedRequestMaximize?()
|
|
||||||
} else {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -230,7 +230,8 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
|
|||||||
|
|
||||||
private var navigationBarOrigin: CGFloat = 0.0
|
private var navigationBarOrigin: CGFloat = 0.0
|
||||||
|
|
||||||
public var modalTopEdgeOffset: CGFloat = 0.0
|
public var minimizedTopEdgeOffset: CGFloat = 0.0
|
||||||
|
public var minimizedBounds: CGRect?
|
||||||
open var isMinimized: Bool = false
|
open var isMinimized: Bool = false
|
||||||
|
|
||||||
open var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? {
|
open var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? {
|
||||||
|
@ -235,9 +235,10 @@ public class ChatMessageStarsMediaInfoNode: ASDisplayNode {
|
|||||||
|
|
||||||
let text: NSMutableAttributedString
|
let text: NSMutableAttributedString
|
||||||
if let peer = arguments.message.peers[arguments.message.id.peerId] as? TelegramChannel, peer.flags.contains(.isCreator) || peer.adminRights != nil {
|
if let peer = arguments.message.peers[arguments.message.id.peerId] as? TelegramChannel, peer.flags.contains(.isCreator) || peer.adminRights != nil {
|
||||||
text = NSMutableAttributedString(string: "⭐️\(arguments.media.amount)", font: textFont, textColor: .white)
|
let amountString = presentationStringsFormattedNumber(Int32(arguments.media.amount), arguments.presentationData.dateTimeFormat.groupingSeparator)
|
||||||
|
text = NSMutableAttributedString(string: "⭐️\(amountString)", font: textFont, textColor: .white)
|
||||||
} else {
|
} else {
|
||||||
text = NSMutableAttributedString(string: "Purchased", font: textFont, textColor: .white)
|
text = NSMutableAttributedString(string: arguments.presentationData.strings.Chat_PaidMedia_Purchased, font: textFont, textColor: .white)
|
||||||
}
|
}
|
||||||
|
|
||||||
var offset: CGFloat = 0.0
|
var offset: CGFloat = 0.0
|
||||||
|
@ -130,7 +130,8 @@ public class ChatMessageUnlockMediaNode: ASDisplayNode {
|
|||||||
let textFont = Font.medium(fontSize)
|
let textFont = Font.medium(fontSize)
|
||||||
|
|
||||||
let padding: CGFloat = 10.0
|
let padding: CGFloat = 10.0
|
||||||
let text = NSMutableAttributedString(string: arguments.presentationData.strings.Chat_UnlockMedia("⭐️ \(arguments.media.amount)").string, font: textFont, textColor: .white)
|
let amountString = presentationStringsFormattedNumber(Int32(arguments.media.amount), arguments.presentationData.dateTimeFormat.groupingSeparator)
|
||||||
|
let text = NSMutableAttributedString(string: arguments.presentationData.strings.Chat_PaidMedia_UnlockMedia("⭐️ \(amountString)").string, font: textFont, textColor: .white)
|
||||||
if let range = text.string.range(of: "⭐️") {
|
if let range = text.string.range(of: "⭐️") {
|
||||||
text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: text.string))
|
text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: text.string))
|
||||||
text.addAttribute(.baselineOffset, value: 0.5, range: NSRange(range, in: text.string))
|
text.addAttribute(.baselineOffset, value: 0.5, range: NSRange(range, in: text.string))
|
||||||
|
23
submodules/TelegramUI/Components/MinimizedContainer/BUILD
Normal file
23
submodules/TelegramUI/Components/MinimizedContainer/BUILD
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "MinimizedContainer",
|
||||||
|
module_name = "MinimizedContainer",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit",
|
||||||
|
"//submodules/AsyncDisplayKit",
|
||||||
|
"//submodules/Display",
|
||||||
|
"//submodules/TelegramPresentationData",
|
||||||
|
"//submodules/ComponentFlow",
|
||||||
|
"//submodules/AccountContext",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
@ -0,0 +1,142 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramPresentationData
|
||||||
|
|
||||||
|
final class MinimizedHeaderNode: ASDisplayNode {
|
||||||
|
var theme: NavigationControllerTheme
|
||||||
|
let strings: PresentationStrings
|
||||||
|
|
||||||
|
private let minimizedBackgroundNode: ASDisplayNode
|
||||||
|
private let minimizedTitleNode: ImmediateTextNode
|
||||||
|
private let minimizedCloseButton: HighlightableButtonNode
|
||||||
|
private var minimizedTitleDisposable: Disposable?
|
||||||
|
|
||||||
|
private var _controllers: [Weak<ViewController>] = []
|
||||||
|
var controllers: [ViewController] {
|
||||||
|
get {
|
||||||
|
return self._controllers.compactMap { $0.value }
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if !newValue.isEmpty {
|
||||||
|
if newValue.count != self.controllers.count {
|
||||||
|
self._controllers = newValue.map { Weak($0) }
|
||||||
|
|
||||||
|
self.minimizedTitleDisposable?.dispose()
|
||||||
|
self.minimizedTitleDisposable = nil
|
||||||
|
|
||||||
|
var signals: [Signal<String?, NoError>] = []
|
||||||
|
for controller in newValue {
|
||||||
|
signals.append(controller.titleSignal)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.minimizedTitleDisposable = (combineLatest(signals)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] titles in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let titles = titles.compactMap { $0 }
|
||||||
|
if titles.count == 1, let title = titles.first {
|
||||||
|
self.title = title
|
||||||
|
} else if let title = titles.last {
|
||||||
|
let othersString = self.strings.WebApp_MinimizedTitle_Others(Int32(titles.count - 1))
|
||||||
|
self.title = self.strings.WebApp_MinimizedTitleFormat(title, othersString).string
|
||||||
|
} else {
|
||||||
|
self.title = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.minimizedTitleDisposable?.dispose()
|
||||||
|
self.minimizedTitleDisposable = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var title: String? {
|
||||||
|
didSet {
|
||||||
|
if let (size, insets) = self.validLayout {
|
||||||
|
self.update(size: size, insets: insets, transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestClose: () -> Void = {}
|
||||||
|
var requestMaximize: () -> Void = {}
|
||||||
|
|
||||||
|
private var validLayout: (CGSize, UIEdgeInsets)?
|
||||||
|
|
||||||
|
init(theme: NavigationControllerTheme, strings: PresentationStrings) {
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
|
||||||
|
self.minimizedBackgroundNode = ASDisplayNode()
|
||||||
|
self.minimizedBackgroundNode.cornerRadius = 10.0
|
||||||
|
self.minimizedBackgroundNode.clipsToBounds = true
|
||||||
|
self.minimizedBackgroundNode.backgroundColor = theme.navigationBar.opaqueBackgroundColor
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
self.minimizedBackgroundNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||||
|
}
|
||||||
|
|
||||||
|
self.minimizedTitleNode = ImmediateTextNode()
|
||||||
|
|
||||||
|
self.minimizedCloseButton = HighlightableButtonNode()
|
||||||
|
self.minimizedCloseButton.setImage(UIImage(bundleImageName: "Instant View/Close"), for: .normal)
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.clipsToBounds = true
|
||||||
|
|
||||||
|
self.addSubnode(self.minimizedBackgroundNode)
|
||||||
|
self.minimizedBackgroundNode.addSubnode(self.minimizedTitleNode)
|
||||||
|
self.minimizedBackgroundNode.addSubnode(self.minimizedCloseButton)
|
||||||
|
|
||||||
|
self.minimizedCloseButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside)
|
||||||
|
|
||||||
|
applySmoothRoundedCorners(self.minimizedBackgroundNode.layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.minimizedTitleDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didLoad() {
|
||||||
|
super.didLoad()
|
||||||
|
|
||||||
|
self.minimizedBackgroundNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.maximizeTapGesture(_:))))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func closePressed() {
|
||||||
|
self.requestClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func maximizeTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
if case .ended = recognizer.state {
|
||||||
|
let location = recognizer.location(in: self.view)
|
||||||
|
if location.x < 48.0 {
|
||||||
|
self.requestClose()
|
||||||
|
} else {
|
||||||
|
self.requestMaximize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
|
||||||
|
self.validLayout = (size, insets)
|
||||||
|
|
||||||
|
let headerHeight: CGFloat = 44.0
|
||||||
|
let titleSideInset: CGFloat = 56.0 + insets.left
|
||||||
|
|
||||||
|
self.minimizedTitleNode.attributedText = NSAttributedString(string: title ?? "", font: Font.bold(17.0), textColor: self.theme.navigationBar.primaryTextColor)
|
||||||
|
|
||||||
|
let titleSize = self.minimizedTitleNode.updateLayout(CGSize(width: size.width - titleSideInset * 2.0, height: headerHeight))
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((headerHeight - titleSize.height) / 2.0)), size: titleSize)
|
||||||
|
self.minimizedTitleNode.bounds = CGRect(origin: .zero, size: titleFrame.size)
|
||||||
|
transition.updatePosition(node: self.minimizedTitleNode, position: titleFrame.center)
|
||||||
|
transition.updateFrame(node: self.minimizedCloseButton, frame: CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: CGSize(width: 44.0, height: 44.0)))
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.minimizedBackgroundNode, frame: CGRect(origin: .zero, size: CGSize(width: size.width, height: 243.0)))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,926 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramPresentationData
|
||||||
|
import ComponentFlow
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
|
private let minimizedNavigationHeight: CGFloat = 44.0
|
||||||
|
private let minimizedTopMargin: CGFloat = 3.0
|
||||||
|
|
||||||
|
final class ScrollViewImpl: UIScrollView {
|
||||||
|
var passthrough = false
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
let result = super.hitTest(point, with: event)
|
||||||
|
if result === self && self.passthrough {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScrollViewDelegate, ASGestureRecognizerDelegate {
|
||||||
|
final class Item {
|
||||||
|
let id: AnyHashable
|
||||||
|
let controller: ViewController
|
||||||
|
|
||||||
|
init(id: AnyHashable, controller: ViewController) {
|
||||||
|
self.id = id
|
||||||
|
self.controller = controller
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ItemNode: ASDisplayNode {
|
||||||
|
var theme: PresentationTheme {
|
||||||
|
didSet {
|
||||||
|
if self.theme !== oldValue {
|
||||||
|
self.headerNode.theme = NavigationControllerTheme(presentationTheme: self.theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let item: Item
|
||||||
|
private let containerNode: ASDisplayNode
|
||||||
|
private let headerNode: MinimizedHeaderNode
|
||||||
|
private let dimCoverNode: ASDisplayNode
|
||||||
|
private let shadowNode: ASImageNode
|
||||||
|
|
||||||
|
var tapped: (() -> Void)?
|
||||||
|
var highlighted: ((Bool) -> Void)?
|
||||||
|
var closeTapped: (() -> Void)?
|
||||||
|
|
||||||
|
var isCovered: Bool = false {
|
||||||
|
didSet {
|
||||||
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||||
|
transition.updateAlpha(node: self.dimCoverNode, alpha: self.isCovered ? 0.25 : 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var isExpanded = false
|
||||||
|
|
||||||
|
private var validLayout: (CGSize, UIEdgeInsets)?
|
||||||
|
|
||||||
|
init(theme: PresentationTheme, strings: PresentationStrings, item: Item) {
|
||||||
|
self.theme = theme
|
||||||
|
self.item = item
|
||||||
|
|
||||||
|
self.shadowNode = ASImageNode()
|
||||||
|
self.shadowNode.clipsToBounds = true
|
||||||
|
self.shadowNode.cornerRadius = 10.0
|
||||||
|
self.shadowNode.displaysAsynchronously = false
|
||||||
|
self.shadowNode.displayWithoutProcessing = true
|
||||||
|
self.shadowNode.contentMode = .scaleToFill
|
||||||
|
self.shadowNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.containerNode = ASDisplayNode()
|
||||||
|
self.containerNode.isUserInteractionEnabled = false
|
||||||
|
self.containerNode.cornerRadius = 10.0
|
||||||
|
|
||||||
|
self.headerNode = MinimizedHeaderNode(theme: NavigationControllerTheme(presentationTheme: theme), strings: strings)
|
||||||
|
self.headerNode.layer.allowsGroupOpacity = true
|
||||||
|
|
||||||
|
self.dimCoverNode = ASDisplayNode()
|
||||||
|
self.dimCoverNode.alpha = 0.0
|
||||||
|
self.dimCoverNode.backgroundColor = UIColor.black
|
||||||
|
self.dimCoverNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.clipsToBounds = true
|
||||||
|
self.cornerRadius = 10.0
|
||||||
|
applySmoothRoundedCorners(self.layer)
|
||||||
|
applySmoothRoundedCorners(self.containerNode.layer)
|
||||||
|
|
||||||
|
self.shadowNode.image = shadowImage
|
||||||
|
|
||||||
|
self.addSubnode(self.containerNode)
|
||||||
|
self.containerNode.addSubnode(self.item.controller.displayNode)
|
||||||
|
self.addSubnode(self.headerNode)
|
||||||
|
self.addSubnode(self.dimCoverNode)
|
||||||
|
self.addSubnode(self.shadowNode)
|
||||||
|
|
||||||
|
self.headerNode.requestClose = { [weak self] in
|
||||||
|
if let self {
|
||||||
|
self.closeTapped?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.headerNode.requestMaximize = { [weak self] in
|
||||||
|
if let self {
|
||||||
|
self.tapped?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.headerNode.controllers = [item.controller]
|
||||||
|
}
|
||||||
|
|
||||||
|
func setTitleControllers(_ controllers: [ViewController]?) {
|
||||||
|
self.headerNode.controllers = controllers ?? [self.item.controller]
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateIn() {
|
||||||
|
self.headerNode.alpha = 0.0
|
||||||
|
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
|
||||||
|
alphaTransition.updateAlpha(node: self.headerNode, alpha: 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isDismissed = false
|
||||||
|
func animateOut() {
|
||||||
|
self.isDismissed = true
|
||||||
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
|
||||||
|
transition.updateAlpha(node: self.headerNode, alpha: 0.0)
|
||||||
|
transition.updateAlpha(node: self.shadowNode, alpha: 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didLoad() {
|
||||||
|
super.didLoad()
|
||||||
|
|
||||||
|
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
||||||
|
recognizer.tapActionAtPoint = { point in
|
||||||
|
return .waitForSingleTap
|
||||||
|
}
|
||||||
|
recognizer.highlight = { [weak self] point in
|
||||||
|
if let point = point, point.x > 280.0 {
|
||||||
|
self?.highlighted?(true)
|
||||||
|
} else {
|
||||||
|
self?.highlighted?(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.view.addGestureRecognizer(recognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||||
|
guard let (_, insets) = self.validLayout else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch recognizer.state {
|
||||||
|
case .ended:
|
||||||
|
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||||
|
switch gesture {
|
||||||
|
case .tap:
|
||||||
|
if location.x < insets.left + minimizedNavigationHeight && location.y < minimizedNavigationHeight {
|
||||||
|
self.closeTapped?()
|
||||||
|
} else {
|
||||||
|
self.tapped?()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
|
||||||
|
self.validLayout = (size, insets)
|
||||||
|
|
||||||
|
var topInset = insets.top
|
||||||
|
if size.width < size.height {
|
||||||
|
topInset += 10.0
|
||||||
|
}
|
||||||
|
self.containerNode.frame = CGRect(origin: .zero, size: size)
|
||||||
|
self.containerNode.subnodeTransform = CATransform3DMakeTranslation(0.0, -topInset, 0.0)
|
||||||
|
|
||||||
|
self.shadowNode.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: size.height - topInset))
|
||||||
|
|
||||||
|
var navigationHeight: CGFloat = minimizedNavigationHeight
|
||||||
|
if !self.isExpanded {
|
||||||
|
navigationHeight += insets.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
let headerFrame = CGRect(origin: .zero, size: CGSize(width: size.width, height: navigationHeight))
|
||||||
|
self.headerNode.update(size: size, insets: insets, transition: transition)
|
||||||
|
transition.updateFrame(node: self.headerNode, frame: headerFrame)
|
||||||
|
transition.updateFrame(node: self.dimCoverNode, frame: CGRect(origin: .zero, size: size))
|
||||||
|
|
||||||
|
if !self.isDismissed {
|
||||||
|
transition.updateAlpha(node: self.shadowNode, alpha: self.isExpanded ? 1.0 : 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let context: AccountContext
|
||||||
|
private weak var navigationController: NavigationController?
|
||||||
|
private var items: [Item] = []
|
||||||
|
|
||||||
|
private var presentationData: PresentationData
|
||||||
|
private var presentationDataDisposable: Disposable?
|
||||||
|
|
||||||
|
var isExpanded: Bool = false
|
||||||
|
public var willMaximize: (() -> Void)?
|
||||||
|
|
||||||
|
private let bottomEdgeView: UIImageView
|
||||||
|
private let blurView: BlurView
|
||||||
|
private let dimView: UIView
|
||||||
|
private let scrollView: ScrollViewImpl
|
||||||
|
private var itemNodes: [AnyHashable: ItemNode] = [:]
|
||||||
|
|
||||||
|
private var highlightedItemId: AnyHashable?
|
||||||
|
|
||||||
|
private var dismissGestureRecognizer: UIPanGestureRecognizer?
|
||||||
|
private var dismissingItemId: AnyHashable?
|
||||||
|
private var dismissingItemOffset: CGFloat?
|
||||||
|
|
||||||
|
private var currentTransition: Transition?
|
||||||
|
private var validLayout: ContainerViewLayout?
|
||||||
|
|
||||||
|
public init(context: AccountContext, navigationController: NavigationController) {
|
||||||
|
self.context = context
|
||||||
|
self.navigationController = navigationController
|
||||||
|
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
|
self.bottomEdgeView = UIImageView()
|
||||||
|
self.bottomEdgeView.contentMode = .scaleToFill
|
||||||
|
self.bottomEdgeView.image = generateImage(CGSize(width: 22.0, height: 24.0), rotatedContext: { size, context in
|
||||||
|
context.setFillColor(UIColor.black.cgColor)
|
||||||
|
context.fill(CGRect(origin: .zero, size: size))
|
||||||
|
|
||||||
|
context.setBlendMode(.clear)
|
||||||
|
context.setFillColor(UIColor.clear.cgColor)
|
||||||
|
|
||||||
|
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: -10, width: 22, height: 20), cornerRadius: 10)
|
||||||
|
context.addPath(path.cgPath)
|
||||||
|
context.fillPath()
|
||||||
|
})?.stretchableImage(withLeftCapWidth: 11, topCapHeight: 12)
|
||||||
|
|
||||||
|
self.blurView = BlurView(effect: nil)
|
||||||
|
self.dimView = UIView()
|
||||||
|
self.dimView.alpha = 0.0
|
||||||
|
self.dimView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.6)
|
||||||
|
self.dimView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.scrollView = ScrollViewImpl()
|
||||||
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
self.scrollView.alwaysBounceVertical = true
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.view.addSubview(self.bottomEdgeView)
|
||||||
|
self.view.addSubview(self.blurView)
|
||||||
|
self.view.addSubview(self.dimView)
|
||||||
|
self.view.addSubview(self.scrollView)
|
||||||
|
|
||||||
|
self.presentationDataDisposable = (self.context.sharedContext.presentationData
|
||||||
|
|> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.presentationData = presentationData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.presentationDataDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func didLoad() {
|
||||||
|
super.didLoad()
|
||||||
|
|
||||||
|
self.scrollView.delegate = self.wrappedScrollViewDelegate
|
||||||
|
self.scrollView.alwaysBounceVertical = true
|
||||||
|
self.scrollView.showsVerticalScrollIndicator = false
|
||||||
|
self.scrollView.showsHorizontalScrollIndicator = false
|
||||||
|
|
||||||
|
let dismissGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.dismissPan(_:)))
|
||||||
|
dismissGestureRecognizer.delegate = self.wrappedGestureRecognizerDelegate
|
||||||
|
dismissGestureRecognizer.delaysTouchesBegan = true
|
||||||
|
self.scrollView.addGestureRecognizer(dismissGestureRecognizer)
|
||||||
|
self.dismissGestureRecognizer = dismissGestureRecognizer
|
||||||
|
}
|
||||||
|
|
||||||
|
func item(at y: CGFloat) -> Int? {
|
||||||
|
guard let layout = self.validLayout else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let insets = layout.insets(options: [.statusBar])
|
||||||
|
let itemCount = self.items.count
|
||||||
|
let spacing = interitemSpacing(itemCount: itemCount, boundingSize: self.scrollView.bounds.size, insets: insets)
|
||||||
|
return max(0, min(Int(floor((y - additionalInsetTop) / spacing)), itemCount - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let location = panGesture.location(in: gestureRecognizer.view)
|
||||||
|
let velocity = panGesture.velocity(in: gestureRecognizer.view)
|
||||||
|
|
||||||
|
if abs(velocity.x) > abs(velocity.y), let _ = self.item(at: location.y) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func dismissPan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||||
|
let scrollView = self.scrollView
|
||||||
|
|
||||||
|
switch gestureRecognizer.state {
|
||||||
|
case .began:
|
||||||
|
let location = gestureRecognizer.location(in: scrollView)
|
||||||
|
guard let item = self.item(at: location.y) else { return }
|
||||||
|
|
||||||
|
self.dismissingItemId = self.items[item].id
|
||||||
|
case .changed:
|
||||||
|
guard let _ = self.dismissingItemId else { return }
|
||||||
|
|
||||||
|
var delta = gestureRecognizer.translation(in: scrollView)
|
||||||
|
delta.y = 0
|
||||||
|
|
||||||
|
if let offset = self.dismissingItemOffset {
|
||||||
|
self.dismissingItemOffset = offset + delta.x
|
||||||
|
} else {
|
||||||
|
self.dismissingItemOffset = delta.x
|
||||||
|
}
|
||||||
|
|
||||||
|
gestureRecognizer.setTranslation(.zero, in: scrollView)
|
||||||
|
|
||||||
|
self.requestUpdate(transition: .immediate)
|
||||||
|
case .ended:
|
||||||
|
var needsLayout = true
|
||||||
|
if let itemId = self.dismissingItemId {
|
||||||
|
if let offset = self.dismissingItemOffset {
|
||||||
|
if offset < -self.frame.width / 4.0 {
|
||||||
|
self.currentTransition = .dismiss(itemId: itemId)
|
||||||
|
|
||||||
|
self.items.removeAll(where: { $0.id == itemId })
|
||||||
|
if self.items.count == 1 {
|
||||||
|
self.isExpanded = false
|
||||||
|
self.willMaximize?()
|
||||||
|
needsLayout = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.dismissingItemOffset = nil
|
||||||
|
self.dismissingItemId = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if needsLayout {
|
||||||
|
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
|
||||||
|
}
|
||||||
|
case .cancelled, .failed:
|
||||||
|
self.dismissingItemId = nil
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
let result = super.hitTest(point, with: event)
|
||||||
|
if result === self.view {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
public func addController(_ viewController: ViewController, transition: ContainedViewLayoutTransition) {
|
||||||
|
let item = Item(
|
||||||
|
id: AnyHashable(Int64.random(in: Int64.min ... Int64.max)),
|
||||||
|
controller: viewController
|
||||||
|
)
|
||||||
|
self.items.append(item)
|
||||||
|
|
||||||
|
self.currentTransition = .minimize(itemId: item.id)
|
||||||
|
self.requestUpdate(transition: transition)
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Transition: Equatable {
|
||||||
|
case minimize(itemId: AnyHashable)
|
||||||
|
case maximize(itemId: AnyHashable)
|
||||||
|
case dismiss(itemId: AnyHashable)
|
||||||
|
case dismissAll
|
||||||
|
|
||||||
|
func matches(item: Item) -> Bool {
|
||||||
|
switch self {
|
||||||
|
case .minimize:
|
||||||
|
return false
|
||||||
|
case let .maximize(itemId), let .dismiss(itemId):
|
||||||
|
return item.id == itemId
|
||||||
|
case .dismissAll:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func maximizeController(_ viewController: ViewController, animated: Bool, completion: @escaping (Bool) -> Void) {
|
||||||
|
guard let item = self.items.first(where: { $0.controller === viewController }) else {
|
||||||
|
completion(self.items.count == 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !animated {
|
||||||
|
self.items.removeAll(where: { $0.id == item.id })
|
||||||
|
self.itemNodes[item.id]?.removeFromSupernode()
|
||||||
|
self.itemNodes[item.id] = nil
|
||||||
|
completion(self.items.count == 0)
|
||||||
|
self.scrollView.contentOffset = .zero
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.isExpanded = false
|
||||||
|
self.currentTransition = .maximize(itemId: item.id)
|
||||||
|
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] _ in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion(self.items.count == 0)
|
||||||
|
self.scrollView.contentOffset = .zero
|
||||||
|
})
|
||||||
|
self.items.removeAll(where: { $0.id == item.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
public func dismissAll(completion: @escaping () -> Void) {
|
||||||
|
self.currentTransition = .dismissAll
|
||||||
|
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring), completion: { _ in
|
||||||
|
completion()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
guard self.isExpanded else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.requestUpdate(transition: .immediate)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestUpdate(transition: ContainedViewLayoutTransition, completion: @escaping (Transition) -> Void = { _ in }) {
|
||||||
|
guard let layout = self.validLayout else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.updateLayout(layout, transition: transition, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||||
|
self.updateLayout(layout, transition: transition, completion: { _ in })
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, completion: @escaping (Transition) -> Void = { _ in }) {
|
||||||
|
let isFirstTime = self.validLayout == nil
|
||||||
|
var containerTransition = transition
|
||||||
|
if isFirstTime {
|
||||||
|
containerTransition = .immediate
|
||||||
|
}
|
||||||
|
|
||||||
|
self.validLayout = layout
|
||||||
|
|
||||||
|
let bounds = CGRect(origin: .zero, size: layout.size)
|
||||||
|
|
||||||
|
containerTransition.updateFrame(view: self.blurView, frame: bounds)
|
||||||
|
containerTransition.updateFrame(view: self.dimView, frame: bounds)
|
||||||
|
if self.isExpanded {
|
||||||
|
if self.blurView.effect == nil {
|
||||||
|
UIView.animate(withDuration: 0.25, animations: {
|
||||||
|
self.blurView.effect = UIBlurEffect(style: self.presentationData.theme.overallDarkAppearance ? .dark : .light)
|
||||||
|
self.dimView.alpha = 1.0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if self.blurView.effect != nil {
|
||||||
|
UIView.animate(withDuration: 0.25, animations: {
|
||||||
|
self.blurView.effect = nil
|
||||||
|
self.dimView.alpha = 0.0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.blurView.isUserInteractionEnabled = self.isExpanded
|
||||||
|
|
||||||
|
let bottomEdgeHeight = 24.0 + 33.0 + layout.intrinsicInsets.bottom
|
||||||
|
let bottomEdgeOrigin = layout.size.height - bottomEdgeHeight
|
||||||
|
containerTransition.updateFrame(view: self.bottomEdgeView, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomEdgeHeight), size: CGSize(width: layout.size.width, height: bottomEdgeHeight)))
|
||||||
|
|
||||||
|
if isFirstTime {
|
||||||
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||||
|
transition.animatePosition(layer: self.bottomEdgeView.layer, from: self.bottomEdgeView.layer.position.offsetBy(dx: 0.0, dy: minimizedNavigationHeight + minimizedTopMargin), to: self.bottomEdgeView.layer.position)
|
||||||
|
}
|
||||||
|
|
||||||
|
let insets = layout.insets(options: [.statusBar])
|
||||||
|
let itemInsets = UIEdgeInsets(top: insets.top, left: layout.safeInsets.left, bottom: insets.bottom, right: layout.safeInsets.right)
|
||||||
|
var topInset = insets.top
|
||||||
|
if layout.size.width < layout.size.height {
|
||||||
|
topInset += 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = 0
|
||||||
|
let contentHeight = frameForIndex(index: self.items.count - 1, size: layout.size, insets: itemInsets, itemCount: self.items.count, boundingSize: layout.size).midY - 70.0
|
||||||
|
for item in self.items {
|
||||||
|
if let currentTransition = self.currentTransition {
|
||||||
|
if currentTransition.matches(item: item) {
|
||||||
|
continue
|
||||||
|
} else if case .dismiss = currentTransition, self.items.count == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemTransition = containerTransition
|
||||||
|
|
||||||
|
let itemNode: ItemNode
|
||||||
|
if let current = self.itemNodes[item.id] {
|
||||||
|
itemNode = current
|
||||||
|
itemNode.theme = self.presentationData.theme
|
||||||
|
} else {
|
||||||
|
itemTransition = .immediate
|
||||||
|
itemNode = ItemNode(theme: self.presentationData.theme, strings: self.presentationData.strings, item: item)
|
||||||
|
self.scrollView.addSubnode(itemNode)
|
||||||
|
self.itemNodes[item.id] = itemNode
|
||||||
|
}
|
||||||
|
itemNode.closeTapped = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.isExpanded {
|
||||||
|
var needsLayout = true
|
||||||
|
self.currentTransition = .dismiss(itemId: item.id)
|
||||||
|
|
||||||
|
self.items.removeAll(where: { $0.id == item.id })
|
||||||
|
if self.items.count == 1 {
|
||||||
|
self.isExpanded = false
|
||||||
|
self.willMaximize?()
|
||||||
|
needsLayout = false
|
||||||
|
}
|
||||||
|
if needsLayout {
|
||||||
|
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.navigationController?.dismissMinimizedControllers(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemNode.tapped = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.isExpanded {
|
||||||
|
self.navigationController?.maximizeViewController(item.controller, animated: true)
|
||||||
|
} else {
|
||||||
|
if self.items.count == 1 {
|
||||||
|
self.navigationController?.maximizeViewController(item.controller, animated: true)
|
||||||
|
} else {
|
||||||
|
self.isExpanded = true
|
||||||
|
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemFrame: CGRect
|
||||||
|
let itemTransform: CATransform3D
|
||||||
|
|
||||||
|
if index == self.items.count - 1 {
|
||||||
|
itemNode.layer.zPosition = 10000.0
|
||||||
|
} else {
|
||||||
|
itemNode.layer.zPosition = 0.0
|
||||||
|
}
|
||||||
|
itemNode.isExpanded = self.isExpanded
|
||||||
|
|
||||||
|
if self.isExpanded {
|
||||||
|
let currentItemFrame = frameForIndex(index: index, size: layout.size, insets: itemInsets, itemCount: self.items.count, boundingSize: layout.size)
|
||||||
|
let currentItemTransform = final3dTransform(for: currentItemFrame.minY, size: currentItemFrame.size, contentHeight: contentHeight, itemCount: self.items.count, additionalAngle: self.highlightedItemId == item.id ? 0.04 : nil, scrollBounds: self.scrollView.bounds, insets: itemInsets)
|
||||||
|
|
||||||
|
var effectiveItemFrame = currentItemFrame
|
||||||
|
var effectiveItemTransform = currentItemTransform
|
||||||
|
|
||||||
|
if let dismissingItemId = self.dismissingItemId, let deletingIndex = self.items.firstIndex(where: { $0.id == dismissingItemId }), let offset = self.dismissingItemOffset {
|
||||||
|
var targetItemFrame: CGRect?
|
||||||
|
var targetItemTransform: CATransform3D?
|
||||||
|
if deletingIndex == index {
|
||||||
|
let effectiveOffset: CGFloat
|
||||||
|
if offset <= 0.0 {
|
||||||
|
effectiveOffset = offset
|
||||||
|
} else {
|
||||||
|
effectiveOffset = scrollingRubberBandingOffset(offset: offset, bandingStart: 0.0, range: 20.0)
|
||||||
|
}
|
||||||
|
effectiveItemFrame = effectiveItemFrame.offsetBy(dx: effectiveOffset, dy: 0.0)
|
||||||
|
} else if index < deletingIndex {
|
||||||
|
let frame = frameForIndex(index: index, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size)
|
||||||
|
let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets)
|
||||||
|
|
||||||
|
targetItemFrame = frame
|
||||||
|
targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets)
|
||||||
|
} else {
|
||||||
|
let frame = frameForIndex(index: index - 1, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size)
|
||||||
|
let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets)
|
||||||
|
|
||||||
|
targetItemFrame = frame
|
||||||
|
targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let targetItemFrame, let targetItemTransform {
|
||||||
|
let fraction = max(0.0, min(1.0, -1.0 * offset / (layout.size.width * 1.5)))
|
||||||
|
effectiveItemFrame = effectiveItemFrame.interpolate(with: targetItemFrame, fraction: fraction)
|
||||||
|
effectiveItemTransform = effectiveItemTransform.interpolate(with: targetItemTransform, fraction: fraction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemFrame = effectiveItemFrame
|
||||||
|
itemTransform = effectiveItemTransform
|
||||||
|
|
||||||
|
itemNode.isCovered = false
|
||||||
|
} else {
|
||||||
|
var itemOffset: CGFloat = bottomEdgeOrigin + 13.0
|
||||||
|
var hideTransform = false
|
||||||
|
if let currentTransition = self.currentTransition {
|
||||||
|
if case let .maximize(itemId) = currentTransition {
|
||||||
|
itemOffset += layout.size.height * 0.25
|
||||||
|
if let lastItemNode = self.scrollView.subviews.last?.asyncdisplaykit_node as? ItemNode, lastItemNode.item.id == itemId {
|
||||||
|
hideTransform = true
|
||||||
|
}
|
||||||
|
} else if case .dismiss = currentTransition, self.items.count == 1 {
|
||||||
|
itemOffset += layout.size.height * 0.25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var effectiveItemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemOffset), size: layout.size)
|
||||||
|
var effectiveItemTransform = itemNode.transform
|
||||||
|
if hideTransform {
|
||||||
|
effectiveItemTransform = CATransform3DMakeScale(0.7, 0.7, 1.0)
|
||||||
|
} else if index == self.items.count - 1 {
|
||||||
|
if self.items.count > 1 {
|
||||||
|
effectiveItemFrame = effectiveItemFrame.offsetBy(dx: 0.0, dy: 4.0)
|
||||||
|
}
|
||||||
|
effectiveItemTransform = CATransform3DIdentity
|
||||||
|
} else {
|
||||||
|
let sideInset: CGFloat = 10.0
|
||||||
|
let scaledWidth = layout.size.width - sideInset * 2.0
|
||||||
|
let scale = scaledWidth / layout.size.width
|
||||||
|
let scaledHeight = layout.size.height * scale
|
||||||
|
let verticalOffset = layout.size.height - scaledHeight
|
||||||
|
effectiveItemFrame = effectiveItemFrame.offsetBy(dx: 0.0, dy: -verticalOffset / 2.0)
|
||||||
|
effectiveItemTransform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||||
|
}
|
||||||
|
itemFrame = effectiveItemFrame
|
||||||
|
itemTransform = effectiveItemTransform
|
||||||
|
|
||||||
|
itemNode.isCovered = index == self.items.count - 2
|
||||||
|
}
|
||||||
|
|
||||||
|
itemNode.bounds = CGRect(origin: .zero, size: itemFrame.size)
|
||||||
|
itemNode.updateLayout(size: layout.size, insets: itemInsets, transition: itemTransition)
|
||||||
|
|
||||||
|
if index == self.items.count - 1 && !self.isExpanded {
|
||||||
|
itemNode.setTitleControllers(self.items.map { $0.controller })
|
||||||
|
} else {
|
||||||
|
itemNode.setTitleControllers(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemTransition.updateTransform(node: itemNode, transform: itemTransform)
|
||||||
|
itemTransition.updatePosition(node: itemNode, position: itemFrame.center)
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentSize = CGSize(width: layout.size.width, height: contentHeight)
|
||||||
|
if self.scrollView.contentSize != contentSize {
|
||||||
|
self.scrollView.contentSize = contentSize
|
||||||
|
}
|
||||||
|
if self.scrollView.frame != bounds {
|
||||||
|
self.scrollView.frame = bounds
|
||||||
|
}
|
||||||
|
self.scrollView.passthrough = !self.isExpanded
|
||||||
|
self.scrollView.isScrollEnabled = self.isExpanded
|
||||||
|
|
||||||
|
if let currentTransition = self.currentTransition {
|
||||||
|
switch self.currentTransition {
|
||||||
|
case let .minimize(itemId):
|
||||||
|
guard let itemNode = self.itemNodes[itemId] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let dimView = UIView()
|
||||||
|
dimView.alpha = 1.0
|
||||||
|
dimView.frame = CGRect(origin: .zero, size: layout.size)
|
||||||
|
dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
||||||
|
self.view.insertSubview(dimView, aboveSubview: self.blurView)
|
||||||
|
dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||||
|
dimView.removeFromSuperview()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
itemNode.animateIn()
|
||||||
|
|
||||||
|
var initialOffset = insets.top + itemNode.item.controller.minimizedTopEdgeOffset
|
||||||
|
if layout.size.width < layout.size.height {
|
||||||
|
initialOffset += 10.0
|
||||||
|
}
|
||||||
|
if let minimizedBounds = itemNode.item.controller.minimizedBounds {
|
||||||
|
initialOffset += -minimizedBounds.minY
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.animatePosition(node: itemNode, from: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + initialOffset), completion: { _ in
|
||||||
|
if self.currentTransition == currentTransition {
|
||||||
|
self.currentTransition = nil
|
||||||
|
}
|
||||||
|
completion(currentTransition)
|
||||||
|
})
|
||||||
|
case let .maximize(itemId):
|
||||||
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||||
|
guard let itemNode = self.itemNodes[itemId] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let dimView = UIView()
|
||||||
|
dimView.frame = CGRect(origin: .zero, size: layout.size)
|
||||||
|
dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
||||||
|
self.view.insertSubview(dimView, aboveSubview: self.blurView)
|
||||||
|
dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||||
|
|
||||||
|
itemNode.animateOut()
|
||||||
|
transition.updateTransform(node: itemNode, transform: CATransform3DIdentity)
|
||||||
|
transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in
|
||||||
|
if self.currentTransition == currentTransition {
|
||||||
|
self.currentTransition = nil
|
||||||
|
}
|
||||||
|
completion(currentTransition)
|
||||||
|
self.itemNodes[itemId] = nil
|
||||||
|
itemNode.removeFromSupernode()
|
||||||
|
dimView.removeFromSuperview()
|
||||||
|
|
||||||
|
self.requestUpdate(transition: .immediate)
|
||||||
|
})
|
||||||
|
case let .dismiss(itemId):
|
||||||
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||||
|
guard let dismissedItemNode = self.itemNodes[itemId] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.items.count == 1 {
|
||||||
|
if let itemNode = self.itemNodes.first(where: { $0.0 != itemId })?.value {
|
||||||
|
let dimView = UIView()
|
||||||
|
dimView.frame = CGRect(origin: .zero, size: layout.size)
|
||||||
|
dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
||||||
|
self.view.insertSubview(dimView, aboveSubview: self.blurView)
|
||||||
|
dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||||
|
|
||||||
|
itemNode.animateOut()
|
||||||
|
transition.updateTransform(node: itemNode, transform: CATransform3DIdentity)
|
||||||
|
transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in
|
||||||
|
if self.currentTransition == currentTransition {
|
||||||
|
self.currentTransition = nil
|
||||||
|
}
|
||||||
|
completion(currentTransition)
|
||||||
|
self.itemNodes[itemId] = nil
|
||||||
|
itemNode.removeFromSupernode()
|
||||||
|
dimView.removeFromSuperview()
|
||||||
|
|
||||||
|
self.navigationController?.maximizeViewController(itemNode.item.controller, animated: false)
|
||||||
|
|
||||||
|
self.requestUpdate(transition: .immediate)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y))
|
||||||
|
} else {
|
||||||
|
transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y), completion: { _ in
|
||||||
|
if self.currentTransition == currentTransition {
|
||||||
|
self.currentTransition = nil
|
||||||
|
}
|
||||||
|
completion(currentTransition)
|
||||||
|
|
||||||
|
self.itemNodes[itemId] = nil
|
||||||
|
dismissedItemNode.removeFromSupernode()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case .dismissAll:
|
||||||
|
let dismissOffset = collapsedHeight(layout: layout)
|
||||||
|
transition.updatePosition(layer: self.bottomEdgeView.layer, position: self.bottomEdgeView.layer.position.offsetBy(dx: 0.0, dy: dismissOffset), completion: { _ in
|
||||||
|
if self.currentTransition == currentTransition {
|
||||||
|
self.currentTransition = nil
|
||||||
|
}
|
||||||
|
completion(currentTransition)
|
||||||
|
})
|
||||||
|
transition.updatePosition(layer: self.scrollView.layer, position: self.scrollView.center.offsetBy(dx: 0.0, dy: dismissOffset))
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collapsedHeight(layout: ContainerViewLayout) -> CGFloat {
|
||||||
|
return minimizedNavigationHeight + minimizedTopMargin + layout.intrinsicInsets.bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let maxInteritemSpacing: CGFloat = 240.0
|
||||||
|
private let additionalInsetTop: CGFloat = 16.0
|
||||||
|
private let additionalInsetBottom: CGFloat = 0.0
|
||||||
|
private let zOffset: CGFloat = -60.0
|
||||||
|
|
||||||
|
private let perspectiveCorrection: CGFloat = -1.0 / 1000.0
|
||||||
|
private let maxRotationAngle: CGFloat = -CGFloat.pi / 2.2
|
||||||
|
|
||||||
|
private func angle(for origin: CGFloat, itemCount: Int, scrollBounds: CGRect, contentHeight: CGFloat?, insets: UIEdgeInsets) -> CGFloat {
|
||||||
|
var rotationAngle = rotationAngleAt0(itemCount: itemCount)
|
||||||
|
|
||||||
|
var contentOffset = scrollBounds.origin.y
|
||||||
|
if contentOffset < 0.0 {
|
||||||
|
contentOffset *= 2.0
|
||||||
|
}
|
||||||
|
|
||||||
|
var yOnScreen = origin - contentOffset - additionalInsetTop - insets.top
|
||||||
|
if yOnScreen < 0 {
|
||||||
|
yOnScreen = 0
|
||||||
|
} else if yOnScreen > scrollBounds.height {
|
||||||
|
yOnScreen = scrollBounds.height
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxRotationVariance = maxRotationAngle - rotationAngleAt0(itemCount: itemCount)
|
||||||
|
rotationAngle += (maxRotationVariance / scrollBounds.height) * yOnScreen
|
||||||
|
|
||||||
|
return rotationAngle
|
||||||
|
}
|
||||||
|
|
||||||
|
private func final3dTransform(for origin: CGFloat, size: CGSize, contentHeight: CGFloat?, itemCount: Int, forcedAngle: CGFloat? = nil, additionalAngle: CGFloat? = nil, scrollBounds: CGRect, insets: UIEdgeInsets) -> CATransform3D {
|
||||||
|
var transform = CATransform3DIdentity
|
||||||
|
transform.m34 = perspectiveCorrection
|
||||||
|
|
||||||
|
let rotationAngle = forcedAngle ?? angle(for: origin, itemCount: itemCount, scrollBounds: scrollBounds, contentHeight: contentHeight, insets: insets)
|
||||||
|
var effectiveRotationAngle = rotationAngle
|
||||||
|
if let additionalAngle = additionalAngle {
|
||||||
|
effectiveRotationAngle += additionalAngle
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = size.height / 2.0 + abs(zOffset / sin(rotationAngle))
|
||||||
|
|
||||||
|
let zTranslation = r * sin(rotationAngle)
|
||||||
|
let yTranslation: CGFloat = r * (1 - cos(rotationAngle))
|
||||||
|
|
||||||
|
let zTranslateTransform = CATransform3DTranslate(transform, 0.0, -yTranslation, zTranslation)
|
||||||
|
|
||||||
|
let rotateTransform = CATransform3DRotate(zTranslateTransform, effectiveRotationAngle, 1.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
return rotateTransform
|
||||||
|
}
|
||||||
|
|
||||||
|
private func interitemSpacing(itemCount: Int, boundingSize: CGSize, insets: UIEdgeInsets) -> CGFloat {
|
||||||
|
var interitemSpacing = maxInteritemSpacing
|
||||||
|
if itemCount > 0 {
|
||||||
|
interitemSpacing = (boundingSize.height - additionalInsetTop - additionalInsetBottom - insets.top) / CGFloat(min(itemCount, 5))
|
||||||
|
}
|
||||||
|
return interitemSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
private func frameForIndex(index: Int, size: CGSize, insets: UIEdgeInsets, itemCount: Int, boundingSize: CGSize) -> CGRect {
|
||||||
|
let spacing = interitemSpacing(itemCount: itemCount, boundingSize: boundingSize, insets: insets)
|
||||||
|
let y = additionalInsetTop + insets.top + spacing * CGFloat(index)
|
||||||
|
let origin = CGPoint(x: insets.left, y: y)
|
||||||
|
|
||||||
|
return CGRect(origin: origin, size: CGSize(width: size.width - insets.left - insets.right, height: size.height))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func rotationAngleAt0(itemCount: Int) -> CGFloat {
|
||||||
|
let multiplier: CGFloat = min(CGFloat(itemCount), 5.0) - 1.0
|
||||||
|
return -CGFloat.pi / 7.0 - CGFloat.pi / 7.0 * multiplier / 4.0
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BlurView: UIVisualEffectView {
|
||||||
|
private func setup() {
|
||||||
|
for subview in self.subviews {
|
||||||
|
if subview.description.contains("VisualEffectSubview") {
|
||||||
|
subview.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let sublayer = self.layer.sublayers?[0], let filters = sublayer.filters {
|
||||||
|
sublayer.backgroundColor = nil
|
||||||
|
sublayer.isOpaque = false
|
||||||
|
let allowedKeys: [String] = [
|
||||||
|
"gaussianBlur",
|
||||||
|
"colorSaturate"
|
||||||
|
]
|
||||||
|
sublayer.filters = filters.filter { filter in
|
||||||
|
guard let filter = filter as? NSObject else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
let filterName = String(describing: filter)
|
||||||
|
if !allowedKeys.contains(filterName) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var effect: UIVisualEffect? {
|
||||||
|
get {
|
||||||
|
return super.effect
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
super.effect = newValue
|
||||||
|
self.setup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didAddSubview(_ subview: UIView) {
|
||||||
|
super.didAddSubview(subview)
|
||||||
|
self.setup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let shadowImage: UIImage? = {
|
||||||
|
return generateImage(CGSize(width: 1.0, height: 480.0), rotatedContext: { size, context in
|
||||||
|
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||||
|
context.clear(bounds)
|
||||||
|
|
||||||
|
let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor] as CFArray
|
||||||
|
|
||||||
|
var locations: [CGFloat] = [0.0, 0.65, 1.0]
|
||||||
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||||
|
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
||||||
|
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: bounds.height), options: [])
|
||||||
|
})
|
||||||
|
}()
|
@ -0,0 +1,52 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension CATransform3D {
|
||||||
|
func interpolate(with other: CATransform3D, fraction: CGFloat) -> CATransform3D {
|
||||||
|
var vectors = Array<CGFloat>(repeating: 0.0, count: 16)
|
||||||
|
vectors[0] = self.m11 + (other.m11 - self.m11) * fraction
|
||||||
|
vectors[1] = self.m12 + (other.m12 - self.m12) * fraction
|
||||||
|
vectors[2] = self.m13 + (other.m13 - self.m13) * fraction
|
||||||
|
vectors[3] = self.m14 + (other.m14 - self.m14) * fraction
|
||||||
|
vectors[4] = self.m21 + (other.m21 - self.m21) * fraction
|
||||||
|
vectors[5] = self.m22 + (other.m22 - self.m22) * fraction
|
||||||
|
vectors[6] = self.m23 + (other.m23 - self.m23) * fraction
|
||||||
|
vectors[7] = self.m24 + (other.m24 - self.m24) * fraction
|
||||||
|
vectors[8] = self.m31 + (other.m31 - self.m31) * fraction
|
||||||
|
vectors[9] = self.m32 + (other.m32 - self.m32) * fraction
|
||||||
|
vectors[10] = self.m33 + (other.m33 - self.m33) * fraction
|
||||||
|
vectors[11] = self.m34 + (other.m34 - self.m34) * fraction
|
||||||
|
vectors[12] = self.m41 + (other.m41 - self.m41) * fraction
|
||||||
|
vectors[13] = self.m42 + (other.m42 - self.m42) * fraction
|
||||||
|
vectors[14] = self.m43 + (other.m43 - self.m43) * fraction
|
||||||
|
vectors[15] = self.m44 + (other.m44 - self.m44) * fraction
|
||||||
|
|
||||||
|
return CATransform3D(m11: vectors[0], m12: vectors[1], m13: vectors[2], m14: vectors[3], m21: vectors[4], m22: vectors[5], m23: vectors[6], m24: vectors[7], m31: vectors[8], m32: vectors[9], m33: vectors[10], m34: vectors[11], m41: vectors[12], m42: vectors[13], m43: vectors[14], m44: vectors[15])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension CGFloat {
|
||||||
|
func interpolate(with other: CGFloat, fraction: CGFloat) -> CGFloat {
|
||||||
|
let invT = 1.0 - fraction
|
||||||
|
let result = other * fraction + self * invT
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension CGPoint {
|
||||||
|
func interpolate(with other: CGPoint, fraction: CGFloat) -> CGPoint {
|
||||||
|
return CGPoint(x: self.x.interpolate(with: other.x, fraction: fraction), y: self.y.interpolate(with: other.y, fraction: fraction))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension CGSize {
|
||||||
|
func interpolate(with other: CGSize, fraction: CGFloat) -> CGSize {
|
||||||
|
return CGSize(width: self.width.interpolate(with: other.width, fraction: fraction), height: self.height.interpolate(with: other.height, fraction: fraction))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CGRect {
|
||||||
|
func interpolate(with other: CGRect, fraction: CGFloat) -> CGRect {
|
||||||
|
return CGRect(origin: self.origin.interpolate(with: other.origin, fraction: fraction), size: self.size.interpolate(with: other.size, fraction: fraction))
|
||||||
|
}
|
||||||
|
}
|
@ -229,7 +229,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
|
|||||||
|
|
||||||
let textString: String
|
let textString: String
|
||||||
if let _ = context.component.requiredStars {
|
if let _ = context.component.requiredStars {
|
||||||
textString = strings.Stars_Purchase_StarsNeededInfo(state.peer?.compactDisplayTitle ?? "").string
|
textString = state.peer == nil ? strings.Stars_Purchase_StarsNeededUnlockInfo : strings.Stars_Purchase_StarsNeededInfo(state.peer?.compactDisplayTitle ?? "").string
|
||||||
} else {
|
} else {
|
||||||
textString = strings.Stars_Purchase_GetStarsInfo
|
textString = strings.Stars_Purchase_GetStarsInfo
|
||||||
}
|
}
|
||||||
@ -310,11 +310,11 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
|
|||||||
minimumCount = requiredStars - balance
|
minimumCount = requiredStars - balance
|
||||||
}
|
}
|
||||||
for product in products {
|
for product in products {
|
||||||
if let minimumCount, minimumCount > product.option.count {
|
if let minimumCount, minimumCount > product.option.count && !(items.isEmpty && product.id == products.last?.id) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if let _ = minimumCount, items.isEmpty {
|
if let _ = minimumCount, items.isEmpty {
|
||||||
|
|
||||||
} else if !context.component.expanded && !initialValues.contains(product.option.count) {
|
} else if !context.component.expanded && !initialValues.contains(product.option.count) {
|
||||||
continue
|
continue
|
||||||
@ -381,7 +381,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !context.component.expanded {
|
if !context.component.expanded && items.count > 1 {
|
||||||
let titleComponent = AnyComponent(MultilineTextComponent(
|
let titleComponent = AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(
|
text: .plain(NSAttributedString(
|
||||||
string: strings.Stars_Purchase_ShowMore,
|
string: strings.Stars_Purchase_ShowMore,
|
||||||
|
@ -213,7 +213,16 @@ private final class SheetContent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue).start(next: { _ in
|
|> deliverOnMainQueue).start(next: { _ in
|
||||||
action()
|
Queue.mainQueue().after(0.1, { [weak self] in
|
||||||
|
if let self, let balance = self.balance, balance < self.invoice.totalAmount {
|
||||||
|
self.inProgress = false
|
||||||
|
self.updated()
|
||||||
|
|
||||||
|
self.buy(requestTopUp: requestTopUp, completion: completion)
|
||||||
|
} else {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -437,7 +446,8 @@ private final class SheetContent: CombinedComponent {
|
|||||||
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, theme)
|
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
let buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amount)", font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center)
|
let amountString = presentationStringsFormattedNumber(Int32(amount), presentationData.dateTimeFormat.groupingSeparator)
|
||||||
|
let buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amountString)", font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center)
|
||||||
if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 {
|
if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 {
|
||||||
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
|
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
|
||||||
buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string))
|
buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string))
|
||||||
@ -450,6 +460,7 @@ private final class SheetContent: CombinedComponent {
|
|||||||
let starsContext = component.starsContext
|
let starsContext = component.starsContext
|
||||||
let botTitle = state.botPeer?.compactDisplayTitle ?? ""
|
let botTitle = state.botPeer?.compactDisplayTitle ?? ""
|
||||||
let invoice = component.invoice
|
let invoice = component.invoice
|
||||||
|
let isMedia = !component.extendedMedia.isEmpty
|
||||||
let button = button.update(
|
let button = button.update(
|
||||||
component: ButtonComponent(
|
component: ButtonComponent(
|
||||||
background: ButtonComponent.Background(
|
background: ButtonComponent.Background(
|
||||||
@ -472,7 +483,7 @@ private final class SheetContent: CombinedComponent {
|
|||||||
context: accountContext,
|
context: accountContext,
|
||||||
starsContext: starsContext,
|
starsContext: starsContext,
|
||||||
options: state?.options ?? [],
|
options: state?.options ?? [],
|
||||||
peerId: state?.botPeer?.id,
|
peerId: isMedia ? nil : state?.botPeer?.id,
|
||||||
requiredStars: invoice.totalAmount,
|
requiredStars: invoice.totalAmount,
|
||||||
completion: { [weak starsContext] stars in
|
completion: { [weak starsContext] stars in
|
||||||
starsContext?.add(balance: stars)
|
starsContext?.add(balance: stars)
|
||||||
|
@ -3752,6 +3752,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
return state.updatedShowWebView(true).updatedForceInputCommandsHidden(true)
|
return state.updatedShowWebView(true).updatedForceInputCommandsHidden(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let context = strongSelf.context
|
||||||
let params = WebAppParameters(source: .menu, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false)
|
let params = WebAppParameters(source: .menu, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false)
|
||||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||||
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
||||||
@ -3798,7 +3799,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, getNavigationController: { [weak self] in
|
}, getNavigationController: { [weak self] in
|
||||||
return self?.effectiveNavigationController
|
return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||||
})
|
})
|
||||||
controller.navigationPresentation = .flatModal
|
controller.navigationPresentation = .flatModal
|
||||||
strongSelf.push(controller)
|
strongSelf.push(controller)
|
||||||
@ -3823,6 +3824,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let context = strongSelf.context
|
||||||
let params = WebAppParameters(source: isInline ? .inline : .simple, peerId: peerId, botId: botId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false)
|
let params = WebAppParameters(source: isInline ? .inline : .simple, peerId: peerId, botId: botId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false)
|
||||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||||
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
||||||
@ -3843,7 +3845,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, getNavigationController: { [weak self] in
|
}, getNavigationController: { [weak self] in
|
||||||
return self?.effectiveNavigationController
|
return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||||
})
|
})
|
||||||
controller.navigationPresentation = .flatModal
|
controller.navigationPresentation = .flatModal
|
||||||
strongSelf.currentWebAppController = controller
|
strongSelf.currentWebAppController = controller
|
||||||
@ -3863,13 +3865,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let context = strongSelf.context
|
||||||
let params = WebAppParameters(source: .generic, peerId: peerId, botId: peerId, botName: botName, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false)
|
let params = WebAppParameters(source: .generic, peerId: peerId, botId: peerId, botName: botName, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false)
|
||||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||||
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
||||||
}, completion: { [weak self] in
|
}, completion: { [weak self] in
|
||||||
self?.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
self?.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||||
}, getNavigationController: { [weak self] in
|
}, getNavigationController: { [weak self] in
|
||||||
return self?.effectiveNavigationController
|
return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||||
})
|
})
|
||||||
controller.navigationPresentation = .flatModal
|
controller.navigationPresentation = .flatModal
|
||||||
strongSelf.currentWebAppController = controller
|
strongSelf.currentWebAppController = controller
|
||||||
@ -8188,6 +8191,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let context = strongSelf.context
|
||||||
let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, url: url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings))
|
let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, url: url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings))
|
||||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||||
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
||||||
@ -8210,7 +8214,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}, completion: { [weak self] in
|
}, completion: { [weak self] in
|
||||||
self?.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
self?.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||||
}, getNavigationController: { [weak self] in
|
}, getNavigationController: { [weak self] in
|
||||||
return self?.effectiveNavigationController
|
return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||||
})
|
})
|
||||||
controller.navigationPresentation = .flatModal
|
controller.navigationPresentation = .flatModal
|
||||||
strongSelf.currentWebAppController = controller
|
strongSelf.currentWebAppController = controller
|
||||||
|
@ -2196,7 +2196,11 @@ public func standaloneWebAppController(
|
|||||||
controller.getSourceRect = getSourceRect
|
controller.getSourceRect = getSourceRect
|
||||||
controller.title = params.botName
|
controller.title = params.botName
|
||||||
controller.shouldMinimizeOnSwipe = {
|
controller.shouldMinimizeOnSwipe = {
|
||||||
return false
|
if params.source != .menu {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user