Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2021-04-09 23:11:54 +03:00
commit 18e66ecea2
16 changed files with 420 additions and 279 deletions

View File

@ -23,6 +23,7 @@ swift_library(
"//submodules/AppBundle:AppBundle", "//submodules/AppBundle:AppBundle",
"//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/OverlayStatusController:OverlayStatusController", "//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/ShimmerEffect:ShimmerEffect",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -3,91 +3,37 @@ import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import PassKit import PassKit
import ShimmerEffect
enum BotCheckoutActionButtonState: Equatable { enum BotCheckoutActionButtonState: Equatable {
case loading
case active(String) case active(String)
case inactive(String)
case applePay case applePay
case placeholder
static func ==(lhs: BotCheckoutActionButtonState, rhs: BotCheckoutActionButtonState) -> Bool {
switch lhs {
case .loading:
if case .loading = rhs {
return true
} else {
return false
}
case let .active(title):
if case .active(title) = rhs {
return true
} else {
return false
}
case let .inactive(title):
if case .inactive(title) = rhs {
return true
} else {
return false
}
case .applePay:
if case .applePay = rhs {
return true
} else {
return false
}
}
}
} }
private let titleFont = Font.semibold(17.0) private let titleFont = Font.semibold(17.0)
final class BotCheckoutActionButton: HighlightableButtonNode { final class BotCheckoutActionButton: HighlightableButtonNode {
static var height: CGFloat = 52.0 static var height: CGFloat = 52.0
private var inactiveFillColor: UIColor
private var activeFillColor: UIColor private var activeFillColor: UIColor
private var foregroundColor: UIColor private var foregroundColor: UIColor
private let progressBackgroundNode: ASImageNode
private let inactiveBackgroundNode: ASImageNode
private let activeBackgroundNode: ASImageNode private let activeBackgroundNode: ASImageNode
private var applePayButton: UIButton? private var applePayButton: UIButton?
private let labelNode: TextNode private let labelNode: TextNode
private var state: BotCheckoutActionButtonState? private var state: BotCheckoutActionButtonState?
private var validLayout: CGSize? private var validLayout: (CGRect, CGSize)?
private var placeholderNode: ShimmerEffectNode?
init(inactiveFillColor: UIColor, activeFillColor: UIColor, foregroundColor: UIColor) { init(activeFillColor: UIColor, foregroundColor: UIColor) {
self.inactiveFillColor = inactiveFillColor
self.activeFillColor = activeFillColor self.activeFillColor = activeFillColor
self.foregroundColor = foregroundColor self.foregroundColor = foregroundColor
let diameter: CGFloat = 20.0 let diameter: CGFloat = 20.0
self.progressBackgroundNode = ASImageNode()
self.progressBackgroundNode.displaysAsynchronously = false
self.progressBackgroundNode.displayWithoutProcessing = true
self.progressBackgroundNode.isLayerBacked = true
self.progressBackgroundNode.image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let strokeWidth: CGFloat = 2.0
context.setFillColor(activeFillColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setFillColor(inactiveFillColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: strokeWidth, y: strokeWidth), size: CGSize(width: size.width - strokeWidth * 2.0, height: size.height - strokeWidth * 2.0)))
let cutout: CGFloat = diameter
context.fill(CGRect(origin: CGPoint(x: floor((size.width - cutout) / 2.0), y: 0.0), size: CGSize(width: cutout, height: cutout)))
})
self.inactiveBackgroundNode = ASImageNode()
self.inactiveBackgroundNode.displaysAsynchronously = false
self.inactiveBackgroundNode.displayWithoutProcessing = true
self.inactiveBackgroundNode.isLayerBacked = true
self.inactiveBackgroundNode.image = generateStretchableFilledCircleImage(diameter: diameter, color: self.foregroundColor, strokeColor: activeFillColor, strokeWidth: 2.0)
self.inactiveBackgroundNode.alpha = 0.0
self.activeBackgroundNode = ASImageNode() self.activeBackgroundNode = ASImageNode()
self.activeBackgroundNode.displaysAsynchronously = false self.activeBackgroundNode.displaysAsynchronously = false
self.activeBackgroundNode.displayWithoutProcessing = true self.activeBackgroundNode.displayWithoutProcessing = true
@ -99,9 +45,7 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
self.labelNode.isUserInteractionEnabled = false self.labelNode.isUserInteractionEnabled = false
super.init() super.init()
self.addSubnode(self.progressBackgroundNode)
self.addSubnode(self.inactiveBackgroundNode)
self.addSubnode(self.activeBackgroundNode) self.addSubnode(self.activeBackgroundNode)
self.addSubnode(self.labelNode) self.addSubnode(self.labelNode)
} }
@ -111,136 +55,8 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
let previousState = self.state let previousState = self.state
self.state = state self.state = state
if let validLayout = self.validLayout, let previousState = previousState { if let (absoluteRect, containerSize) = self.validLayout, let previousState = previousState {
switch state { self.updateLayout(absoluteRect: absoluteRect, containerSize: containerSize, transition: .immediate)
case .loading:
self.inactiveBackgroundNode.layer.animateFrame(from: self.inactiveBackgroundNode.frame, to: self.progressBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
if !self.inactiveBackgroundNode.alpha.isZero {
self.inactiveBackgroundNode.alpha = 0.0
self.inactiveBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
}
self.activeBackgroundNode.layer.animateFrame(from: self.activeBackgroundNode.frame, to: self.progressBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.activeBackgroundNode.alpha = 0.0
self.activeBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
self.labelNode.alpha = 0.0
self.labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
self.progressBackgroundNode.alpha = 1.0
self.progressBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
basicAnimation.duration = 0.8
basicAnimation.fromValue = NSNumber(value: Float(0.0))
basicAnimation.toValue = NSNumber(value: Float.pi * 2.0)
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
self.progressBackgroundNode.layer.add(basicAnimation, forKey: "progressRotation")
case let .active(title):
if let applePayButton = self.applePayButton {
self.applePayButton = nil
applePayButton.removeFromSuperview()
}
if case .active = previousState {
let makeLayout = TextNode.asyncLayout(self.labelNode)
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
let _ = labelApply()
} else {
self.inactiveBackgroundNode.layer.animateFrame(from: self.progressBackgroundNode.frame, to: self.activeBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.inactiveBackgroundNode.alpha = 1.0
self.progressBackgroundNode.alpha = 0.0
self.activeBackgroundNode.layer.animateFrame(from: self.progressBackgroundNode.frame, to: self.activeBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.activeBackgroundNode.alpha = 1.0
self.activeBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
let makeLayout = TextNode.asyncLayout(self.labelNode)
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
let _ = labelApply()
self.labelNode.alpha = 1.0
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
case let .inactive(title):
if case .inactive = previousState {
let makeLayout = TextNode.asyncLayout(self.labelNode)
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.activeFillColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
let _ = labelApply()
} else {
self.inactiveBackgroundNode.layer.animateFrame(from: self.inactiveBackgroundNode.frame, to: self.activeBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.inactiveBackgroundNode.alpha = 1.0
self.progressBackgroundNode.alpha = 0.0
self.activeBackgroundNode.alpha = 0.0
let makeLayout = TextNode.asyncLayout(self.labelNode)
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
let _ = labelApply()
self.labelNode.alpha = 1.0
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
case .applePay:
if self.applePayButton == nil {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
let applePayButton: PKPaymentButton
if #available(iOS 14.0, *) {
applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black)
} else {
applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black)
}
applePayButton.addTarget(self, action: #selector(self.applePayButtonPressed), for: .touchUpInside)
self.view.addSubview(applePayButton)
self.applePayButton = applePayButton
}
}
if let applePayButton = self.applePayButton {
applePayButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: validLayout.width, height: BotCheckoutActionButton.height))
}
}
} else {
switch state {
case .loading:
self.labelNode.alpha = 0.0
self.progressBackgroundNode.alpha = 1.0
self.activeBackgroundNode.alpha = 0.0
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
basicAnimation.duration = 0.8
basicAnimation.fromValue = NSNumber(value: Float(0.0))
basicAnimation.toValue = NSNumber(value: Float.pi * 2.0)
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
self.progressBackgroundNode.layer.add(basicAnimation, forKey: "progressRotation")
case .active:
self.labelNode.alpha = 1.0
self.progressBackgroundNode.alpha = 0.0
self.inactiveBackgroundNode.alpha = 0.0
self.activeBackgroundNode.alpha = 1.0
case .inactive:
self.labelNode.alpha = 1.0
self.progressBackgroundNode.alpha = 0.0
self.inactiveBackgroundNode.alpha = 1.0
self.activeBackgroundNode.alpha = 0.0
case .applePay:
self.labelNode.alpha = 0.0
self.progressBackgroundNode.alpha = 0.0
self.inactiveBackgroundNode.alpha = 0.0
self.activeBackgroundNode.alpha = 0.0
if self.applePayButton == nil {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
let applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black)
self.view.addSubview(applePayButton)
self.applePayButton = applePayButton
}
}
}
} }
} }
} }
@ -249,33 +65,81 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
self.sendActions(forControlEvents: .touchUpInside, with: nil) self.sendActions(forControlEvents: .touchUpInside, with: nil)
} }
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { func updateLayout(absoluteRect: CGRect, containerSize: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size let size = absoluteRect.size
transition.updateFrame(node: self.progressBackgroundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - BotCheckoutActionButton.height) / 2.0), y: 0.0), size: CGSize(width: BotCheckoutActionButton.height, height: BotCheckoutActionButton.height))) self.validLayout = (absoluteRect, containerSize)
transition.updateFrame(node: self.inactiveBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.height)))
transition.updateFrame(node: self.activeBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.height))) transition.updateFrame(node: self.activeBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.height)))
if let applePayButton = self.applePayButton {
applePayButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.height))
}
var labelSize = self.labelNode.bounds.size var labelSize = self.labelNode.bounds.size
if let state = self.state { if let state = self.state {
switch state { switch state {
case let .active(title): case let .active(title):
let makeLayout = TextNode.asyncLayout(self.labelNode) if let applePayButton = self.applePayButton {
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) self.applePayButton = nil
let _ = labelApply() applePayButton.removeFromSuperview()
labelSize = labelLayout.size }
case let .inactive(title):
let makeLayout = TextNode.asyncLayout(self.labelNode) if let placeholderNode = self.placeholderNode {
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.activeFillColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) self.placeholderNode = nil
let _ = labelApply() placeholderNode.removeFromSupernode()
labelSize = labelLayout.size }
default:
break let makeLayout = TextNode.asyncLayout(self.labelNode)
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let _ = labelApply()
labelSize = labelLayout.size
case .applePay:
if self.applePayButton == nil {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
let applePayButton: PKPaymentButton
if #available(iOS 14.0, *) {
applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black)
} else {
applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black)
}
applePayButton.addTarget(self, action: #selector(self.applePayButtonPressed), for: .touchUpInside)
self.view.addSubview(applePayButton)
self.applePayButton = applePayButton
}
}
if let placeholderNode = self.placeholderNode {
self.placeholderNode = nil
placeholderNode.removeFromSupernode()
}
if let applePayButton = self.applePayButton {
applePayButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.height))
}
case .placeholder:
if let applePayButton = self.applePayButton {
self.applePayButton = nil
applePayButton.removeFromSuperview()
}
let contentSize = CGSize(width: 80.0, height: 8.0)
let shimmerNode: ShimmerEffectNode
if let current = self.placeholderNode {
shimmerNode = current
} else {
shimmerNode = ShimmerEffectNode()
self.placeholderNode = shimmerNode
self.addSubnode(shimmerNode)
}
shimmerNode.frame = CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize)
shimmerNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: absoluteRect.minX + shimmerNode.frame.minX, y: absoluteRect.minY + shimmerNode.frame.minY), size: contentSize), within: containerSize)
var shapes: [ShimmerEffectNode.Shape] = []
shapes.append(.roundedRectLine(startPoint: CGPoint(x: 0.0, y: 0.0), width: contentSize.width, diameter: contentSize.height))
shimmerNode.update(backgroundColor: self.activeFillColor, foregroundColor: self.activeFillColor.mixedWith(UIColor.white, alpha: 0.25), shimmeringColor: self.activeFillColor.mixedWith(UIColor.white, alpha: 0.15), shapes: shapes, size: contentSize)
} }
} }
transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: floor((size.width - labelSize.width) / 2.0), y: floor((size.height - labelSize.height) / 2.0)), size: labelSize)) transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: floor((size.width - labelSize.width) / 2.0), y: floor((size.height - labelSize.height) / 2.0)), size: labelSize))
} }
} }

View File

@ -10,6 +10,64 @@ import TelegramPresentationData
import AccountContext import AccountContext
public final class BotCheckoutController: ViewController { public final class BotCheckoutController: ViewController {
public final class InputData {
public enum FetchError {
case generic
}
let form: BotPaymentForm
let validatedFormInfo: BotPaymentValidatedFormInfo?
private init(
form: BotPaymentForm,
validatedFormInfo: BotPaymentValidatedFormInfo?
) {
self.form = form
self.validatedFormInfo = validatedFormInfo
}
public static func fetch(context: AccountContext, messageId: MessageId) -> Signal<InputData, FetchError> {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let themeParams: [String: Any] = [
"bg_color": Int32(bitPattern: presentationData.theme.list.plainBackgroundColor.argb),
"text_color": Int32(bitPattern: presentationData.theme.list.itemPrimaryTextColor.argb),
"link_color": Int32(bitPattern: presentationData.theme.list.itemAccentColor.argb),
"button_color": Int32(bitPattern: presentationData.theme.list.itemCheckColors.fillColor.argb),
"button_text_color": Int32(bitPattern: presentationData.theme.list.itemCheckColors.foregroundColor.argb)
]
return fetchBotPaymentForm(postbox: context.account.postbox, network: context.account.network, messageId: messageId, themeParams: themeParams)
|> mapError { _ -> FetchError in
return .generic
}
|> mapToSignal { paymentForm -> Signal<InputData, FetchError> in
if let current = paymentForm.savedInfo {
return validateBotPaymentForm(account: context.account, saveInfo: true, messageId: messageId, formInfo: current)
|> mapError { _ -> FetchError in
return .generic
}
|> map { result -> InputData in
return InputData(
form: paymentForm,
validatedFormInfo: result
)
}
|> `catch` { _ -> Signal<InputData, FetchError> in
return .single(InputData(
form: paymentForm,
validatedFormInfo: nil
))
}
} else {
return .single(InputData(
form: paymentForm,
validatedFormInfo: nil
))
}
}
}
}
private var controllerNode: BotCheckoutControllerNode { private var controllerNode: BotCheckoutControllerNode {
return self.displayNode as! BotCheckoutControllerNode return self.displayNode as! BotCheckoutControllerNode
} }
@ -26,11 +84,14 @@ public final class BotCheckoutController: ViewController {
private var presentationData: PresentationData private var presentationData: PresentationData
private var didPlayPresentationAnimation = false private var didPlayPresentationAnimation = false
private let inputData: Promise<BotCheckoutController.InputData?>
public init(context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId) { public init(context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId, inputData: Promise<BotCheckoutController.InputData?>) {
self.context = context self.context = context
self.invoice = invoice self.invoice = invoice
self.messageId = messageId self.messageId = messageId
self.inputData = inputData
self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -56,7 +117,7 @@ public final class BotCheckoutController: ViewController {
if let strongSelf = self { if let strongSelf = self {
strongSelf.navigationOffset = offset strongSelf.navigationOffset = offset
} }
}, context: self.context, invoice: self.invoice, messageId: self.messageId, present: { [weak self] c, a in }, context: self.context, invoice: self.invoice, messageId: self.messageId, inputData: self.inputData, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a) self?.present(c, in: .window(.root), with: a)
}, dismissAnimated: { [weak self] in }, dismissAnimated: { [weak self] in
self?.dismiss() self?.dismiss()

View File

@ -45,8 +45,21 @@ private enum BotCheckoutSection: Int32 {
} }
enum BotCheckoutEntry: ItemListNodeEntry { enum BotCheckoutEntry: ItemListNodeEntry {
enum StableId: Hashable {
case header
case price(Int)
case actionPlaceholder(Int)
case tip
case paymentMethod
case shippingInfo
case shippingMethod
case nameInfo
case emailInfo
case phoneInfo
}
case header(PresentationTheme, TelegramMediaInvoice, String) case header(PresentationTheme, TelegramMediaInvoice, String)
case price(Int, PresentationTheme, String, String, Bool, Bool) case price(Int, PresentationTheme, String, String, Bool, Bool, Int?)
case tip(Int, PresentationTheme, String, String, String, Int64, Int64, [(String, Int64)]) case tip(Int, PresentationTheme, String, String, String, Int64, Int64, [(String, Int64)])
case paymentMethod(PresentationTheme, String, String) case paymentMethod(PresentationTheme, String, String)
case shippingInfo(PresentationTheme, String, String) case shippingInfo(PresentationTheme, String, String)
@ -54,6 +67,7 @@ enum BotCheckoutEntry: ItemListNodeEntry {
case nameInfo(PresentationTheme, String, String) case nameInfo(PresentationTheme, String, String)
case emailInfo(PresentationTheme, String, String) case emailInfo(PresentationTheme, String, String)
case phoneInfo(PresentationTheme, String, String) case phoneInfo(PresentationTheme, String, String)
case actionPlaceholder(Int, Int)
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
@ -66,14 +80,16 @@ enum BotCheckoutEntry: ItemListNodeEntry {
} }
} }
var stableId: Int32 { var sortId: Int32 {
switch self { switch self {
case .header: case .header:
return 0 return 0
case let .price(index, _, _, _, _, _): case let .price(index, _, _, _, _, _, _):
return 1 + Int32(index) return 1 + Int32(index)
case let .tip(index, _, _, _, _, _, _, _): case let .tip(index, _, _, _, _, _, _, _):
return 1 + Int32(index) return 1 + Int32(index)
case let .actionPlaceholder(index, _):
return 1 + Int32(index)
case .paymentMethod: case .paymentMethod:
return 10000 + 2 return 10000 + 2
case .shippingInfo: case .shippingInfo:
@ -88,6 +104,31 @@ enum BotCheckoutEntry: ItemListNodeEntry {
return 10000 + 7 return 10000 + 7
} }
} }
var stableId: StableId {
switch self {
case .header:
return .header
case let .price(index, _, _, _, _, _, _):
return .price(index)
case .tip:
return .tip
case let .actionPlaceholder(index, _):
return .actionPlaceholder(index)
case .paymentMethod:
return .paymentMethod
case .shippingInfo:
return .shippingInfo
case .shippingMethod:
return .shippingMethod
case .nameInfo:
return .nameInfo
case .emailInfo:
return .emailInfo
case .phoneInfo:
return .phoneInfo
}
}
static func ==(lhs: BotCheckoutEntry, rhs: BotCheckoutEntry) -> Bool { static func ==(lhs: BotCheckoutEntry, rhs: BotCheckoutEntry) -> Bool {
switch lhs { switch lhs {
@ -106,8 +147,8 @@ enum BotCheckoutEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsFinal, lhsHasSeparator): case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsFinal, lhsHasSeparator, lhsShimmeringIndex):
if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsFinal, rhsHasSeparator) = rhs { if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsFinal, rhsHasSeparator, rhsShimmeringIndex) = rhs {
if lhsIndex != rhsIndex { if lhsIndex != rhsIndex {
return false return false
} }
@ -126,6 +167,9 @@ enum BotCheckoutEntry: ItemListNodeEntry {
if lhsHasSeparator != rhsHasSeparator { if lhsHasSeparator != rhsHasSeparator {
return false return false
} }
if lhsShimmeringIndex != rhsShimmeringIndex {
return false
}
return true return true
} else { } else {
return false return false
@ -183,11 +227,17 @@ enum BotCheckoutEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .actionPlaceholder(index, shimmeringIndex):
if case .actionPlaceholder(index, shimmeringIndex) = rhs {
return true
} else {
return false
}
} }
} }
static func <(lhs: BotCheckoutEntry, rhs: BotCheckoutEntry) -> Bool { static func <(lhs: BotCheckoutEntry, rhs: BotCheckoutEntry) -> Bool {
return lhs.stableId < rhs.stableId return lhs.sortId < rhs.sortId
} }
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
@ -195,8 +245,8 @@ enum BotCheckoutEntry: ItemListNodeEntry {
switch self { switch self {
case let .header(theme, invoice, botName): case let .header(theme, invoice, botName):
return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section) return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section)
case let .price(_, theme, text, value, isFinal, hasSeparator): case let .price(_, theme, text, value, isFinal, hasSeparator, shimmeringIndex):
return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: hasSeparator, sectionId: self.section) return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: hasSeparator, shimmeringIndex: shimmeringIndex, sectionId: self.section)
case let .tip(_, _, text, currency, value, numericValue, maxValue, variants): case let .tip(_, _, text, currency, value, numericValue, maxValue, variants):
return BotCheckoutTipItem(theme: presentationData.theme, strings: presentationData.strings, title: text, currency: currency, value: value, numericValue: numericValue, maxValue: maxValue, availableVariants: variants, sectionId: self.section, updateValue: { value in return BotCheckoutTipItem(theme: presentationData.theme, strings: presentationData.strings, title: text, currency: currency, value: value, numericValue: numericValue, maxValue: maxValue, availableVariants: variants, sectionId: self.section, updateValue: { value in
arguments.updateTip(value) arguments.updateTip(value)
@ -229,6 +279,9 @@ enum BotCheckoutEntry: ItemListNodeEntry {
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openInfo(.phone) arguments.openInfo(.phone)
}) })
case let .actionPlaceholder(_, shimmeringIndex):
return ItemListDisclosureItem(presentationData: presentationData, title: " ", label: " ", sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
}, shimmeringIndex: shimmeringIndex)
} }
} }
} }
@ -293,7 +346,7 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st
var index = 0 var index = 0
for price in paymentForm.invoice.prices { for price in paymentForm.invoice.prices {
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false, index == 0)) entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false, index == 0, nil))
totalPrice += price.amount totalPrice += price.amount
index += 1 index += 1
} }
@ -307,7 +360,7 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st
shippingOptionString = option.title shippingOptionString = option.title
for price in option.prices { for price in option.prices {
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false, false)) entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false, false, nil))
totalPrice += price.amount totalPrice += price.amount
index += 1 index += 1
} }
@ -320,8 +373,8 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st
if !entries.isEmpty { if !entries.isEmpty {
switch entries[entries.count - 1] { switch entries[entries.count - 1] {
case let .price(index, theme, title, value, _, _): case let .price(index, theme, title, value, _, _, _):
entries[entries.count - 1] = .price(index, theme, title, value, false, false) entries[entries.count - 1] = .price(index, theme, title, value, false, index == 0, nil)
default: default:
break break
} }
@ -336,7 +389,7 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st
index += 1 index += 1
} }
entries.append(.price(index, presentationData.theme, presentationData.strings.Checkout_TotalAmount, formatCurrencyAmount(totalPrice, currency: paymentForm.invoice.currency), true, true)) entries.append(.price(index, presentationData.theme, presentationData.strings.Checkout_TotalAmount, formatCurrencyAmount(totalPrice, currency: paymentForm.invoice.currency), true, true, nil))
var paymentMethodTitle = "" var paymentMethodTitle = ""
if let currentPaymentMethod = currentPaymentMethod { if let currentPaymentMethod = currentPaymentMethod {
@ -379,6 +432,15 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st
if paymentForm.invoice.requestedFields.contains(.phone) { if paymentForm.invoice.requestedFields.contains(.phone) {
entries.append(.phoneInfo(presentationData.theme, presentationData.strings.Checkout_Phone, formInfo?.phone ?? "")) entries.append(.phoneInfo(presentationData.theme, presentationData.strings.Checkout_Phone, formInfo?.phone ?? ""))
} }
} else {
let numItems = 4
for index in 0 ..< numItems {
entries.append(.price(index, presentationData.theme, " ", " ", false, index == 0, index))
}
for index in numItems ..< numItems + 2 {
entries.append(.actionPlaceholder(index, index - numItems))
}
} }
return entries return entries
@ -474,7 +536,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
private var passwordTip: String? private var passwordTip: String?
private var passwordTipDisposable: Disposable? private var passwordTipDisposable: Disposable?
init(controller: BotCheckoutController?, navigationBar: NavigationBar, updateNavigationOffset: @escaping (CGFloat) -> Void, context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId, present: @escaping (ViewController, Any?) -> Void, dismissAnimated: @escaping () -> Void) { init(controller: BotCheckoutController?, navigationBar: NavigationBar, updateNavigationOffset: @escaping (CGFloat) -> Void, context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId, inputData: Promise<BotCheckoutController.InputData?>, present: @escaping (ViewController, Any?) -> Void, dismissAnimated: @escaping () -> Void) {
self.controller = controller self.controller = controller
self.context = context self.context = context
self.messageId = messageId self.messageId = messageId
@ -514,9 +576,8 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
self.actionButtonPanelSeparator = ASDisplayNode() self.actionButtonPanelSeparator = ASDisplayNode()
self.actionButtonPanelSeparator.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.actionButtonPanelSeparator.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.actionButton = BotCheckoutActionButton(inactiveFillColor: self.presentationData.theme.list.plainBackgroundColor, activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor) self.actionButton = BotCheckoutActionButton(activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor)
self.actionButton.setState(.active("")) self.actionButton.setState(.placeholder)
self.actionButtonPanelNode.isHidden = true
self.inProgressDimNode = ASDisplayNode() self.inProgressDimNode = ASDisplayNode()
self.inProgressDimNode.alpha = 0.0 self.inProgressDimNode.alpha = 0.0
@ -828,56 +889,33 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
}), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} }
} }
let themeParams: [String: Any] = [
"bg_color": Int32(bitPattern: self.presentationData.theme.list.plainBackgroundColor.argb),
"text_color": Int32(bitPattern: self.presentationData.theme.list.itemPrimaryTextColor.argb),
"link_color": Int32(bitPattern: self.presentationData.theme.list.itemAccentColor.argb),
"button_color": Int32(bitPattern: self.presentationData.theme.list.itemCheckColors.fillColor.argb),
"button_text_color": Int32(bitPattern: self.presentationData.theme.list.itemCheckColors.foregroundColor.argb)
]
let formAndMaybeValidatedInfo = fetchBotPaymentForm(postbox: context.account.postbox, network: context.account.network, messageId: messageId, themeParams: themeParams) self.formRequestDisposable = (inputData.get() |> deliverOnMainQueue).start(next: { [weak self] formAndValidatedInfo in
|> mapToSignal { paymentForm -> Signal<(BotPaymentForm, BotPaymentValidatedFormInfo?), BotPaymentFormRequestError> in
if let current = paymentForm.savedInfo {
return validateBotPaymentForm(account: context.account, saveInfo: true, messageId: messageId, formInfo: current)
|> mapError { _ -> BotPaymentFormRequestError in
return .generic
}
|> map { result -> (BotPaymentForm, BotPaymentValidatedFormInfo?) in
return (paymentForm, result)
}
|> `catch` { _ -> Signal<(BotPaymentForm, BotPaymentValidatedFormInfo?), BotPaymentFormRequestError> in
return .single((paymentForm, nil))
}
} else {
return .single((paymentForm, nil))
}
}
self.formRequestDisposable = (formAndMaybeValidatedInfo |> deliverOnMainQueue).start(next: { [weak self] form, validatedInfo in
if let strongSelf = self { if let strongSelf = self {
guard let formAndValidatedInfo = formAndValidatedInfo else {
strongSelf.controller?.dismiss()
return
}
UIView.transition(with: strongSelf.view, duration: 0.25, options: UIView.AnimationOptions.transitionCrossDissolve, animations: { UIView.transition(with: strongSelf.view, duration: 0.25, options: UIView.AnimationOptions.transitionCrossDissolve, animations: {
}, completion: nil) }, completion: nil)
let savedInfo: BotPaymentRequestedInfo let savedInfo: BotPaymentRequestedInfo
if let current = form.savedInfo { if let current = formAndValidatedInfo.form.savedInfo {
savedInfo = current savedInfo = current
} else { } else {
savedInfo = BotPaymentRequestedInfo(name: nil, phone: nil, email: nil, shippingAddress: nil) savedInfo = BotPaymentRequestedInfo(name: nil, phone: nil, email: nil, shippingAddress: nil)
} }
strongSelf.paymentFormValue = form strongSelf.paymentFormValue = formAndValidatedInfo.form
strongSelf.currentFormInfo = savedInfo strongSelf.currentFormInfo = savedInfo
strongSelf.currentValidatedFormInfo = validatedInfo strongSelf.currentValidatedFormInfo = formAndValidatedInfo.validatedFormInfo
if let savedCredentials = form.savedCredentials { if let savedCredentials = formAndValidatedInfo.form.savedCredentials {
strongSelf.currentPaymentMethod = .savedCredentials(savedCredentials) strongSelf.currentPaymentMethod = .savedCredentials(savedCredentials)
} }
strongSelf.actionButton.isEnabled = true strongSelf.actionButton.isEnabled = true
strongSelf.paymentFormAndInfo.set(.single((form, savedInfo, validatedInfo, nil, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount))) strongSelf.paymentFormAndInfo.set(.single((formAndValidatedInfo.form, savedInfo, formAndValidatedInfo.validatedFormInfo, nil, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount)))
strongSelf.updateActionButton() strongSelf.updateActionButton()
} }
}, error: { _ in
}) })
self.addSubnode(self.actionButtonPanelNode) self.addSubnode(self.actionButtonPanelNode)
@ -958,7 +996,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
let actionButtonFrame = CGRect(origin: CGPoint(x: bottomPanelHorizontalInset, y: bottomPanelVerticalInset), size: CGSize(width: layout.size.width - bottomPanelHorizontalInset * 2.0, height: BotCheckoutActionButton.height)) let actionButtonFrame = CGRect(origin: CGPoint(x: bottomPanelHorizontalInset, y: bottomPanelVerticalInset), size: CGSize(width: layout.size.width - bottomPanelHorizontalInset * 2.0, height: BotCheckoutActionButton.height))
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
self.actionButton.updateLayout(size: actionButtonFrame.size, transition: transition) self.actionButton.updateLayout(absoluteRect: actionButtonFrame.offsetBy(dx: self.actionButtonPanelNode.frame.minX, dy: self.actionButtonPanelNode.frame.minY), containerSize: layout.size, transition: transition)
updatedInsets.bottom = bottomPanelHeight updatedInsets.bottom = bottomPanelHeight

View File

@ -6,6 +6,7 @@ import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import ItemListUI import ItemListUI
import PresentationDataUtils import PresentationDataUtils
import ShimmerEffect
class BotCheckoutPriceItem: ListViewItem, ItemListItem { class BotCheckoutPriceItem: ListViewItem, ItemListItem {
let theme: PresentationTheme let theme: PresentationTheme
@ -13,16 +14,18 @@ class BotCheckoutPriceItem: ListViewItem, ItemListItem {
let label: String let label: String
let isFinal: Bool let isFinal: Bool
let hasSeparator: Bool let hasSeparator: Bool
let shimmeringIndex: Int?
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
let requestsNoInset: Bool = true let requestsNoInset: Bool = true
init(theme: PresentationTheme, title: String, label: String, isFinal: Bool, hasSeparator: Bool, sectionId: ItemListSectionId) { init(theme: PresentationTheme, title: String, label: String, isFinal: Bool, hasSeparator: Bool, shimmeringIndex: Int?, sectionId: ItemListSectionId) {
self.theme = theme self.theme = theme
self.title = title self.title = title
self.label = label self.label = label
self.isFinal = isFinal self.isFinal = isFinal
self.hasSeparator = hasSeparator self.hasSeparator = hasSeparator
self.shimmeringIndex = shimmeringIndex
self.sectionId = sectionId self.sectionId = sectionId
} }
@ -89,6 +92,9 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
let backgroundNode: ASDisplayNode let backgroundNode: ASDisplayNode
let separatorNode: ASDisplayNode let separatorNode: ASDisplayNode
let bottomSeparatorNode: ASDisplayNode let bottomSeparatorNode: ASDisplayNode
private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
private var item: BotCheckoutPriceItem? private var item: BotCheckoutPriceItem?
@ -111,6 +117,15 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
self.addSubnode(self.separatorNode) self.addSubnode(self.separatorNode)
self.addSubnode(self.bottomSeparatorNode) self.addSubnode(self.bottomSeparatorNode)
} }
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
var rect = rect
rect.origin.y += self.insets.top
self.absoluteLocation = (rect, containerSize)
if let shimmerNode = self.placeholderNode {
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
}
}
func asyncLayout() -> (_ item: BotCheckoutPriceItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors, _ previousItem: ListViewItem?, _ nextItem: ListViewItem?) -> (ListViewItemNodeLayout, () -> Void) { func asyncLayout() -> (_ item: BotCheckoutPriceItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors, _ previousItem: ListViewItem?, _ nextItem: ListViewItem?) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
@ -124,7 +139,12 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
if item.isFinal { if item.isFinal {
naturalContentHeight = 44.0 naturalContentHeight = 44.0
} else { } else {
naturalContentHeight = 34.0 switch neighbors.bottom {
case .otherSection, .none:
naturalContentHeight = 44.0
default:
naturalContentHeight = 34.0
}
} }
if let _ = previousItem as? BotCheckoutHeaderItem { if let _ = previousItem as? BotCheckoutHeaderItem {
verticalOffset += 8.0 verticalOffset += 8.0
@ -164,7 +184,13 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
strongSelf.separatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.separatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: params.width - leftInset, height: UIScreenPixel)) strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: params.width - leftInset, height: UIScreenPixel))
strongSelf.bottomSeparatorNode.isHidden = !item.isFinal switch neighbors.bottom {
case .otherSection, .none:
strongSelf.bottomSeparatorNode.isHidden = false
default:
strongSelf.bottomSeparatorNode.isHidden = !item.isFinal
}
strongSelf.bottomSeparatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomSeparatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: CGSize(width: params.width, height: UIScreenPixel)) strongSelf.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: CGSize(width: params.width, height: UIScreenPixel))
@ -173,6 +199,38 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset + floor((naturalContentHeight - titleLayout.size.height) / 2.0)), size: titleLayout.size) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset + floor((naturalContentHeight - titleLayout.size.height) / 2.0)), size: titleLayout.size)
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: verticalOffset + floor((naturalContentHeight - labelLayout.size.height) / 2.0)), size: labelLayout.size) strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: verticalOffset + floor((naturalContentHeight - labelLayout.size.height) / 2.0)), size: labelLayout.size)
if let shimmeringIndex = item.shimmeringIndex {
let shimmerNode: ShimmerEffectNode
if let current = strongSelf.placeholderNode {
shimmerNode = current
} else {
shimmerNode = ShimmerEffectNode()
strongSelf.placeholderNode = shimmerNode
if strongSelf.separatorNode.supernode != nil {
strongSelf.insertSubnode(shimmerNode, belowSubnode: strongSelf.separatorNode)
} else {
strongSelf.addSubnode(shimmerNode)
}
}
shimmerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
if let (rect, size) = strongSelf.absoluteLocation {
shimmerNode.updateAbsoluteRect(rect, within: size)
}
var shapes: [ShimmerEffectNode.Shape] = []
let titleLineWidth: CGFloat = (shimmeringIndex % 2 == 0) ? 120.0 : 80.0
let lineDiameter: CGFloat = 8.0
let titleFrame = strongSelf.titleNode.frame
shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter))
shimmerNode.update(backgroundColor: item.theme.list.itemBlocksBackgroundColor, foregroundColor: item.theme.list.mediaPlaceholderColor, shimmeringColor: item.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: contentSize)
} else if let shimmerNode = strongSelf.placeholderNode {
strongSelf.placeholderNode = nil
shimmerNode.removeFromSupernode()
}
} }
}) })
} }

View File

@ -158,7 +158,7 @@ enum BotReceiptEntry: ItemListNodeEntry {
case let .header(theme, invoice, botName): case let .header(theme, invoice, botName):
return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section) return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section)
case let .price(_, theme, text, value, hasSeparator, isFinal): case let .price(_, theme, text, value, hasSeparator, isFinal):
return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: hasSeparator, sectionId: self.section) return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: hasSeparator, shimmeringIndex: nil, sectionId: self.section)
case let .paymentMethod(_, text, value): case let .paymentMethod(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil)
case let .shippingInfo(_, text, value): case let .shippingInfo(_, text, value):
@ -301,7 +301,7 @@ final class BotReceiptControllerNode: ItemListControllerNode {
self.actionButtonPanelSeparator = ASDisplayNode() self.actionButtonPanelSeparator = ASDisplayNode()
self.actionButtonPanelSeparator.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.actionButtonPanelSeparator.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.actionButton = BotCheckoutActionButton(inactiveFillColor: self.presentationData.theme.list.plainBackgroundColor, activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.plainBackgroundColor) self.actionButton = BotCheckoutActionButton(activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.plainBackgroundColor)
self.actionButton.setState(.active(self.presentationData.strings.Common_Done)) self.actionButton.setState(.active(self.presentationData.strings.Common_Done))
super.init(controller: controller, navigationBar: navigationBar, updateNavigationOffset: updateNavigationOffset, state: signal) super.init(controller: controller, navigationBar: navigationBar, updateNavigationOffset: updateNavigationOffset, state: signal)
@ -338,7 +338,7 @@ final class BotReceiptControllerNode: ItemListControllerNode {
let actionButtonFrame = CGRect(origin: CGPoint(x: bottomPanelHorizontalInset, y: bottomPanelVerticalInset), size: CGSize(width: layout.size.width - bottomPanelHorizontalInset * 2.0, height: BotCheckoutActionButton.height)) let actionButtonFrame = CGRect(origin: CGPoint(x: bottomPanelHorizontalInset, y: bottomPanelVerticalInset), size: CGSize(width: layout.size.width - bottomPanelHorizontalInset * 2.0, height: BotCheckoutActionButton.height))
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
self.actionButton.updateLayout(size: actionButtonFrame.size, transition: transition) self.actionButton.updateLayout(absoluteRect: actionButtonFrame.offsetBy(dx: self.actionButtonPanelNode.frame.minX, dy: self.actionButtonPanelNode.frame.minY), containerSize: layout.size, transition: transition)
updatedInsets.bottom = bottomPanelHeight updatedInsets.bottom = bottomPanelHeight

View File

@ -166,6 +166,7 @@ public final class PinchSourceContainerNode: ASDisplayNode, UIGestureRecognizerD
public var activate: ((PinchSourceContainerNode) -> Void)? public var activate: ((PinchSourceContainerNode) -> Void)?
public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
public var animatedOut: (() -> Void)?
var deactivate: (() -> Void)? var deactivate: (() -> Void)?
var updated: ((CGFloat, CGPoint, CGPoint) -> Void)? var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
@ -350,6 +351,8 @@ private final class PinchControllerNode: ViewControllerTracingNode {
strongSelf.sourceNode.restoreToNaturalSize() strongSelf.sourceNode.restoreToNaturalSize()
strongSelf.sourceNode.addSubnode(strongSelf.sourceNode.contentNode) strongSelf.sourceNode.addSubnode(strongSelf.sourceNode.contentNode)
strongSelf.sourceNode.animatedOut?()
completion() completion()
} }

View File

@ -22,6 +22,7 @@ swift_library(
"//submodules/SegmentedControlNode:SegmentedControlNode", "//submodules/SegmentedControlNode:SegmentedControlNode",
"//submodules/AccountContext:AccountContext", "//submodules/AccountContext:AccountContext",
"//submodules/AnimationUI:AnimationUI", "//submodules/AnimationUI:AnimationUI",
"//submodules/ShimmerEffect:ShimmerEffect",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -4,6 +4,7 @@ import Display
import AsyncDisplayKit import AsyncDisplayKit
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import ShimmerEffect
public enum ItemListDisclosureItemTitleColor { public enum ItemListDisclosureItemTitleColor {
case primary case primary
@ -38,8 +39,9 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem {
let action: (() -> Void)? let action: (() -> Void)?
let clearHighlightAutomatically: Bool let clearHighlightAutomatically: Bool
public let tag: ItemListItemTag? public let tag: ItemListItemTag?
public let shimmeringIndex: Int?
public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, title: String, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, label: String, labelStyle: ItemListDisclosureLabelStyle = .text, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil) { public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, title: String, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, label: String, labelStyle: ItemListDisclosureLabelStyle = .text, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) {
self.presentationData = presentationData self.presentationData = presentationData
self.icon = icon self.icon = icon
self.title = title self.title = title
@ -53,6 +55,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem {
self.action = action self.action = action
self.clearHighlightAutomatically = clearHighlightAutomatically self.clearHighlightAutomatically = clearHighlightAutomatically
self.tag = tag self.tag = tag
self.shimmeringIndex = shimmeringIndex
} }
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -131,6 +134,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
public var tag: ItemListItemTag? { public var tag: ItemListItemTag? {
return self.item?.tag return self.item?.tag
} }
private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
public init() { public init() {
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
@ -179,6 +185,15 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
self.addSubnode(self.activateArea) self.addSubnode(self.activateArea)
} }
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
var rect = rect
rect.origin.y += self.insets.top
self.absoluteLocation = (rect, containerSize)
if let shimmerNode = self.placeholderNode {
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
}
}
public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
@ -479,6 +494,38 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
} }
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: height + UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: height + UIScreenPixel))
if let shimmeringIndex = item.shimmeringIndex {
let shimmerNode: ShimmerEffectNode
if let current = strongSelf.placeholderNode {
shimmerNode = current
} else {
shimmerNode = ShimmerEffectNode()
strongSelf.placeholderNode = shimmerNode
if strongSelf.backgroundNode.supernode != nil {
strongSelf.insertSubnode(shimmerNode, aboveSubnode: strongSelf.backgroundNode)
} else {
strongSelf.addSubnode(shimmerNode)
}
}
shimmerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
if let (rect, size) = strongSelf.absoluteLocation {
shimmerNode.updateAbsoluteRect(rect, within: size)
}
var shapes: [ShimmerEffectNode.Shape] = []
let titleLineWidth: CGFloat = (shimmeringIndex % 2 == 0) ? 120.0 : 80.0
let lineDiameter: CGFloat = 8.0
let titleFrame = strongSelf.titleNode.frame
shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter))
shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: contentSize)
} else if let shimmerNode = strongSelf.placeholderNode {
strongSelf.placeholderNode = nil
shimmerNode.removeFromSupernode()
}
} }
}) })
} }

View File

@ -521,12 +521,14 @@ public final class PrincipalThemeAdditionalGraphics {
public let chatBubbleActionButtonIncomingShareIconImage: UIImage public let chatBubbleActionButtonIncomingShareIconImage: UIImage
public let chatBubbleActionButtonIncomingPhoneIconImage: UIImage public let chatBubbleActionButtonIncomingPhoneIconImage: UIImage
public let chatBubbleActionButtonIncomingLocationIconImage: UIImage public let chatBubbleActionButtonIncomingLocationIconImage: UIImage
public let chatBubbleActionButtonIncomingPaymentIconImage: UIImage
public let chatBubbleActionButtonOutgoingMessageIconImage: UIImage public let chatBubbleActionButtonOutgoingMessageIconImage: UIImage
public let chatBubbleActionButtonOutgoingLinkIconImage: UIImage public let chatBubbleActionButtonOutgoingLinkIconImage: UIImage
public let chatBubbleActionButtonOutgoingShareIconImage: UIImage public let chatBubbleActionButtonOutgoingShareIconImage: UIImage
public let chatBubbleActionButtonOutgoingPhoneIconImage: UIImage public let chatBubbleActionButtonOutgoingPhoneIconImage: UIImage
public let chatBubbleActionButtonOutgoingLocationIconImage: UIImage public let chatBubbleActionButtonOutgoingLocationIconImage: UIImage
public let chatBubbleActionButtonOutgoingPaymentIconImage: UIImage
public let chatEmptyItemLockIcon: UIImage public let chatEmptyItemLockIcon: UIImage
public let emptyChatListCheckIcon: UIImage public let emptyChatListCheckIcon: UIImage
@ -565,11 +567,13 @@ public final class PrincipalThemeAdditionalGraphics {
self.chatBubbleActionButtonIncomingShareIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotShare"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonIncomingShareIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotShare"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))!
self.chatBubbleActionButtonIncomingPhoneIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPhone"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonIncomingPhoneIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPhone"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))!
self.chatBubbleActionButtonIncomingLocationIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLocation"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonIncomingLocationIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLocation"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))!
self.chatBubbleActionButtonIncomingPaymentIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPayment"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))!
self.chatBubbleActionButtonOutgoingMessageIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotMessage"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingMessageIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotMessage"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))!
self.chatBubbleActionButtonOutgoingLinkIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingLinkIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))!
self.chatBubbleActionButtonOutgoingShareIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotShare"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingShareIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotShare"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))!
self.chatBubbleActionButtonOutgoingPhoneIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPhone"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingPhoneIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPhone"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))!
self.chatBubbleActionButtonOutgoingLocationIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLocation"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingLocationIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLocation"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))!
self.chatBubbleActionButtonOutgoingPaymentIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPayment"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))!
self.chatEmptyItemLockIcon = generateImage(CGSize(width: 9.0, height: 13.0), rotatedContext: { size, context in self.chatEmptyItemLockIcon = generateImage(CGSize(width: 9.0, height: 13.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "card.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1873,7 +1873,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let receiptMessageId = invoice.receiptMessageId { if let receiptMessageId = invoice.receiptMessageId {
strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} else { } else {
strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, messageId: messageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) let inputData = Promise<BotCheckoutController.InputData?>()
inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, messageId: message.id)
|> map(Optional.init)
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
return .single(nil)
})
strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, messageId: messageId, inputData: inputData), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} }
} }
} }

View File

@ -103,6 +103,8 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLocationIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLocationIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage
case .switchInline: case .switchInline:
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingShareIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage iconImage = incoming ? graphics.chatBubbleActionButtonIncomingShareIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage
case .payment:
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPaymentIconImage : graphics.chatBubbleActionButtonOutgoingPaymentIconImage
default: default:
iconImage = nil iconImage = nil
} }

View File

@ -21,6 +21,7 @@ import RadialStatusNode
import TelegramUIPreferences import TelegramUIPreferences
import PeerInfoAvatarListNode import PeerInfoAvatarListNode
import AnimationUI import AnimationUI
import ContextUI
enum PeerInfoHeaderButtonKey: Hashable { enum PeerInfoHeaderButtonKey: Hashable {
case message case message
@ -771,6 +772,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
final class PeerInfoAvatarListNode: ASDisplayNode { final class PeerInfoAvatarListNode: ASDisplayNode {
private let isSettings: Bool private let isSettings: Bool
let pinchSourceNode: PinchSourceContainerNode
let avatarContainerNode: PeerInfoAvatarTransformContainerNode let avatarContainerNode: PeerInfoAvatarTransformContainerNode
let listContainerTransformNode: ASDisplayNode let listContainerTransformNode: ASDisplayNode
let listContainerNode: PeerInfoAvatarListContainerNode let listContainerNode: PeerInfoAvatarListContainerNode
@ -781,9 +783,12 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
var item: PeerInfoAvatarListItem? var item: PeerInfoAvatarListItem?
var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)? var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)?
var animateOverlaysFadeIn: (() -> Void)?
init(context: AccountContext, readyWhenGalleryLoads: Bool, isSettings: Bool) { init(context: AccountContext, readyWhenGalleryLoads: Bool, isSettings: Bool) {
self.isSettings = isSettings self.isSettings = isSettings
self.pinchSourceNode = PinchSourceContainerNode()
self.avatarContainerNode = PeerInfoAvatarTransformContainerNode(context: context) self.avatarContainerNode = PeerInfoAvatarTransformContainerNode(context: context)
self.listContainerTransformNode = ASDisplayNode() self.listContainerTransformNode = ASDisplayNode()
@ -792,10 +797,11 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
self.listContainerNode.isHidden = true self.listContainerNode.isHidden = true
super.init() super.init()
self.addSubnode(self.avatarContainerNode) self.addSubnode(self.pinchSourceNode)
self.pinchSourceNode.contentNode.addSubnode(self.avatarContainerNode)
self.listContainerTransformNode.addSubnode(self.listContainerNode) self.listContainerTransformNode.addSubnode(self.listContainerNode)
self.addSubnode(self.listContainerTransformNode) self.pinchSourceNode.contentNode.addSubnode(self.listContainerTransformNode)
let avatarReady = (self.avatarContainerNode.avatarNode.ready let avatarReady = (self.avatarContainerNode.avatarNode.ready
|> mapToSignal { _ -> Signal<Bool, NoError> in |> mapToSignal { _ -> Signal<Bool, NoError> in
@ -837,10 +843,29 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
} }
} }
} }
self.pinchSourceNode.activate = { [weak self] sourceNode in
guard let _ = self else {
return
}
let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: {
return UIScreen.main.bounds
})
context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController)
}
self.pinchSourceNode.animatedOut = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.animateOverlaysFadeIn?()
}
} }
func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) {
self.arguments = (peer, theme, avatarSize, isExpanded) self.arguments = (peer, theme, avatarSize, isExpanded)
self.pinchSourceNode.update(size: size, transition: transition)
self.pinchSourceNode.frame = CGRect(origin: CGPoint(), size: size)
self.avatarContainerNode.update(peer: peer, item: self.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: self.isSettings) self.avatarContainerNode.update(peer: peer, item: self.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: self.isSettings)
} }
@ -1634,6 +1659,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
var requestOpenAvatarForEditing: ((Bool) -> Void)? var requestOpenAvatarForEditing: ((Bool) -> Void)?
var cancelUpload: (() -> Void)? var cancelUpload: (() -> Void)?
var requestUpdateLayout: (() -> Void)? var requestUpdateLayout: (() -> Void)?
var animateOverlaysFadeIn: (() -> Void)?
var displayAvatarContextMenu: ((ASDisplayNode, ContextGesture?) -> Void)? var displayAvatarContextMenu: ((ASDisplayNode, ContextGesture?) -> Void)?
var displayCopyContextMenu: ((ASDisplayNode, Bool, Bool) -> Void)? var displayCopyContextMenu: ((ASDisplayNode, Bool, Bool) -> Void)?
@ -1748,6 +1774,17 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} }
strongSelf.editingContentNode.avatarNode.update(peer: peer, item: strongSelf.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing) strongSelf.editingContentNode.avatarNode.update(peer: peer, item: strongSelf.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing)
} }
self.avatarListNode.animateOverlaysFadeIn = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.navigationButtonContainer.layer.animateAlpha(from: 0.0, to: strongSelf.navigationButtonContainer.alpha, duration: 0.25)
strongSelf.avatarListNode.listContainerNode.shadowNode.layer.animateAlpha(from: 0.0, to: strongSelf.avatarListNode.listContainerNode.shadowNode.alpha, duration: 0.25)
strongSelf.avatarListNode.listContainerNode.controlsContainerNode.layer.animateAlpha(from: 0.0, to: strongSelf.avatarListNode.listContainerNode.controlsContainerNode.alpha, duration: 0.25)
strongSelf.animateOverlaysFadeIn?()
}
} }
override func didLoad() { override func didLoad() {

View File

@ -2389,6 +2389,13 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
strongSelf.openAvatarForEditing() strongSelf.openAvatarForEditing()
} }
} }
self.headerNode.animateOverlaysFadeIn = { [weak self] in
guard let strongSelf = self, let navigationBar = strongSelf.controller?.navigationBar else {
return
}
navigationBar.layer.animateAlpha(from: 0.0, to: navigationBar.alpha, duration: 0.25)
}
self.headerNode.requestUpdateLayout = { [weak self] in self.headerNode.requestUpdateLayout = { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {