Web app minimization

This commit is contained in:
Ilya Laktyushin 2024-06-25 22:26:55 +04:00
parent 7d5d9ba7ba
commit 2b1d09fb5a
21 changed files with 1386 additions and 760 deletions

View File

@ -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";

View File

@ -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",

View File

@ -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

View File

@ -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)
} }
} }

View File

@ -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()
}
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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))
} }
} }
} }

View File

@ -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:

View File

@ -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 {
}
}
}
}
} }

View File

@ -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? {

View File

@ -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

View File

@ -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))

View 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",
],
)

View File

@ -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)))
}
}

View File

@ -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: [])
})
}()

View File

@ -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))
}
}

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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
} }