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/PresentationDataUtils:PresentationDataUtils",
"//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/ShimmerEffect:ShimmerEffect",
],
visibility = [
"//visibility:public",

View File

@ -3,41 +3,12 @@ import UIKit
import AsyncDisplayKit
import Display
import PassKit
import ShimmerEffect
enum BotCheckoutActionButtonState: Equatable {
case loading
case active(String)
case inactive(String)
case applePay
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
}
}
}
case placeholder
}
private let titleFont = Font.semibold(17.0)
@ -45,49 +16,24 @@ private let titleFont = Font.semibold(17.0)
final class BotCheckoutActionButton: HighlightableButtonNode {
static var height: CGFloat = 52.0
private var inactiveFillColor: UIColor
private var activeFillColor: UIColor
private var foregroundColor: UIColor
private let progressBackgroundNode: ASImageNode
private let inactiveBackgroundNode: ASImageNode
private let activeBackgroundNode: ASImageNode
private var applePayButton: UIButton?
private let labelNode: TextNode
private var state: BotCheckoutActionButtonState?
private var validLayout: CGSize?
private var validLayout: (CGRect, CGSize)?
init(inactiveFillColor: UIColor, activeFillColor: UIColor, foregroundColor: UIColor) {
self.inactiveFillColor = inactiveFillColor
private var placeholderNode: ShimmerEffectNode?
init(activeFillColor: UIColor, foregroundColor: UIColor) {
self.activeFillColor = activeFillColor
self.foregroundColor = foregroundColor
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.displaysAsynchronously = false
self.activeBackgroundNode.displayWithoutProcessing = true
@ -100,8 +46,6 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
super.init()
self.addSubnode(self.progressBackgroundNode)
self.addSubnode(self.inactiveBackgroundNode)
self.addSubnode(self.activeBackgroundNode)
self.addSubnode(self.labelNode)
}
@ -111,136 +55,8 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
let previousState = self.state
self.state = state
if let validLayout = self.validLayout, let previousState = previousState {
switch state {
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
}
}
}
if let (absoluteRect, containerSize) = self.validLayout, let previousState = previousState {
self.updateLayout(absoluteRect: absoluteRect, containerSize: containerSize, transition: .immediate)
}
}
}
@ -249,33 +65,81 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
self.sendActions(forControlEvents: .touchUpInside, with: nil)
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size
func updateLayout(absoluteRect: CGRect, containerSize: CGSize, transition: ContainedViewLayoutTransition) {
let size = absoluteRect.size
self.validLayout = (absoluteRect, containerSize)
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)))
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)))
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
if let state = self.state {
switch state {
case let .active(title):
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 let .inactive(title):
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: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let _ = labelApply()
labelSize = labelLayout.size
default:
break
case let .active(title):
if let applePayButton = self.applePayButton {
self.applePayButton = nil
applePayButton.removeFromSuperview()
}
if let placeholderNode = self.placeholderNode {
self.placeholderNode = nil
placeholderNode.removeFromSupernode()
}
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))
}
}

View File

@ -10,6 +10,64 @@ import TelegramPresentationData
import AccountContext
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 {
return self.displayNode as! BotCheckoutControllerNode
}
@ -27,10 +85,13 @@ public final class BotCheckoutController: ViewController {
private var didPlayPresentationAnimation = false
public init(context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId) {
private let inputData: Promise<BotCheckoutController.InputData?>
public init(context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId, inputData: Promise<BotCheckoutController.InputData?>) {
self.context = context
self.invoice = invoice
self.messageId = messageId
self.inputData = inputData
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -56,7 +117,7 @@ public final class BotCheckoutController: ViewController {
if let strongSelf = self {
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)
}, dismissAnimated: { [weak self] in
self?.dismiss()

View File

@ -45,8 +45,21 @@ private enum BotCheckoutSection: Int32 {
}
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 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 paymentMethod(PresentationTheme, String, String)
case shippingInfo(PresentationTheme, String, String)
@ -54,6 +67,7 @@ enum BotCheckoutEntry: ItemListNodeEntry {
case nameInfo(PresentationTheme, String, String)
case emailInfo(PresentationTheme, String, String)
case phoneInfo(PresentationTheme, String, String)
case actionPlaceholder(Int, Int)
var section: ItemListSectionId {
switch self {
@ -66,14 +80,16 @@ enum BotCheckoutEntry: ItemListNodeEntry {
}
}
var stableId: Int32 {
var sortId: Int32 {
switch self {
case .header:
return 0
case let .price(index, _, _, _, _, _):
case let .price(index, _, _, _, _, _, _):
return 1 + Int32(index)
case let .tip(index, _, _, _, _, _, _, _):
return 1 + Int32(index)
case let .actionPlaceholder(index, _):
return 1 + Int32(index)
case .paymentMethod:
return 10000 + 2
case .shippingInfo:
@ -89,6 +105,31 @@ enum BotCheckoutEntry: ItemListNodeEntry {
}
}
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 {
switch lhs {
case let .header(lhsTheme, lhsInvoice, lhsName):
@ -106,8 +147,8 @@ enum BotCheckoutEntry: ItemListNodeEntry {
} else {
return false
}
case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsFinal, lhsHasSeparator):
if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsFinal, rhsHasSeparator) = rhs {
case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsFinal, lhsHasSeparator, lhsShimmeringIndex):
if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsFinal, rhsHasSeparator, rhsShimmeringIndex) = rhs {
if lhsIndex != rhsIndex {
return false
}
@ -126,6 +167,9 @@ enum BotCheckoutEntry: ItemListNodeEntry {
if lhsHasSeparator != rhsHasSeparator {
return false
}
if lhsShimmeringIndex != rhsShimmeringIndex {
return false
}
return true
} else {
return false
@ -183,11 +227,17 @@ enum BotCheckoutEntry: ItemListNodeEntry {
} else {
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 {
return lhs.stableId < rhs.stableId
return lhs.sortId < rhs.sortId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
@ -195,8 +245,8 @@ enum BotCheckoutEntry: ItemListNodeEntry {
switch self {
case let .header(theme, invoice, botName):
return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section)
case let .price(_, theme, text, value, isFinal, hasSeparator):
return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: hasSeparator, sectionId: self.section)
case let .price(_, theme, text, value, isFinal, hasSeparator, shimmeringIndex):
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):
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)
@ -229,6 +279,9 @@ enum BotCheckoutEntry: ItemListNodeEntry {
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
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
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
index += 1
}
@ -307,7 +360,7 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st
shippingOptionString = option.title
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
index += 1
}
@ -320,8 +373,8 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st
if !entries.isEmpty {
switch entries[entries.count - 1] {
case let .price(index, theme, title, value, _, _):
entries[entries.count - 1] = .price(index, theme, title, value, false, false)
case let .price(index, theme, title, value, _, _, _):
entries[entries.count - 1] = .price(index, theme, title, value, false, index == 0, nil)
default:
break
}
@ -336,7 +389,7 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st
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 = ""
if let currentPaymentMethod = currentPaymentMethod {
@ -379,6 +432,15 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st
if paymentForm.invoice.requestedFields.contains(.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
@ -474,7 +536,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
private var passwordTip: String?
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.context = context
self.messageId = messageId
@ -514,9 +576,8 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
self.actionButtonPanelSeparator = ASDisplayNode()
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.setState(.active(""))
self.actionButtonPanelNode.isHidden = true
self.actionButton = BotCheckoutActionButton(activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor)
self.actionButton.setState(.placeholder)
self.inProgressDimNode = ASDisplayNode()
self.inProgressDimNode.alpha = 0.0
@ -828,56 +889,33 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
}), 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)
|> 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
self.formRequestDisposable = (inputData.get() |> deliverOnMainQueue).start(next: { [weak self] formAndValidatedInfo in
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: {
}, completion: nil)
let savedInfo: BotPaymentRequestedInfo
if let current = form.savedInfo {
if let current = formAndValidatedInfo.form.savedInfo {
savedInfo = current
} else {
savedInfo = BotPaymentRequestedInfo(name: nil, phone: nil, email: nil, shippingAddress: nil)
}
strongSelf.paymentFormValue = form
strongSelf.paymentFormValue = formAndValidatedInfo.form
strongSelf.currentFormInfo = savedInfo
strongSelf.currentValidatedFormInfo = validatedInfo
if let savedCredentials = form.savedCredentials {
strongSelf.currentValidatedFormInfo = formAndValidatedInfo.validatedFormInfo
if let savedCredentials = formAndValidatedInfo.form.savedCredentials {
strongSelf.currentPaymentMethod = .savedCredentials(savedCredentials)
}
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()
}
}, error: { _ in
})
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))
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

View File

@ -6,6 +6,7 @@ import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import ShimmerEffect
class BotCheckoutPriceItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
@ -13,16 +14,18 @@ class BotCheckoutPriceItem: ListViewItem, ItemListItem {
let label: String
let isFinal: Bool
let hasSeparator: Bool
let shimmeringIndex: Int?
let sectionId: ItemListSectionId
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.title = title
self.label = label
self.isFinal = isFinal
self.hasSeparator = hasSeparator
self.shimmeringIndex = shimmeringIndex
self.sectionId = sectionId
}
@ -90,6 +93,9 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
let separatorNode: ASDisplayNode
let bottomSeparatorNode: ASDisplayNode
private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
private var item: BotCheckoutPriceItem?
init() {
@ -112,6 +118,15 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
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) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
@ -124,7 +139,12 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
if item.isFinal {
naturalContentHeight = 44.0
} else {
naturalContentHeight = 34.0
switch neighbors.bottom {
case .otherSection, .none:
naturalContentHeight = 44.0
default:
naturalContentHeight = 34.0
}
}
if let _ = previousItem as? BotCheckoutHeaderItem {
verticalOffset += 8.0
@ -164,7 +184,13 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
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.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.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.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):
return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section)
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):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil)
case let .shippingInfo(_, text, value):
@ -301,7 +301,7 @@ final class BotReceiptControllerNode: ItemListControllerNode {
self.actionButtonPanelSeparator = ASDisplayNode()
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))
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))
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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ShimmerEffect
public enum ItemListDisclosureItemTitleColor {
case primary
@ -38,8 +39,9 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem {
let action: (() -> Void)?
let clearHighlightAutomatically: Bool
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.icon = icon
self.title = title
@ -53,6 +55,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem {
self.action = action
self.clearHighlightAutomatically = clearHighlightAutomatically
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) {
@ -132,6 +135,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
return self.item?.tag
}
private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
@ -180,6 +186,15 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
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) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
@ -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))
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 chatBubbleActionButtonIncomingPhoneIconImage: UIImage
public let chatBubbleActionButtonIncomingLocationIconImage: UIImage
public let chatBubbleActionButtonIncomingPaymentIconImage: UIImage
public let chatBubbleActionButtonOutgoingMessageIconImage: UIImage
public let chatBubbleActionButtonOutgoingLinkIconImage: UIImage
public let chatBubbleActionButtonOutgoingShareIconImage: UIImage
public let chatBubbleActionButtonOutgoingPhoneIconImage: UIImage
public let chatBubbleActionButtonOutgoingLocationIconImage: UIImage
public let chatBubbleActionButtonOutgoingPaymentIconImage: UIImage
public let chatEmptyItemLockIcon: 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.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.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.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.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.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
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 {
strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} 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
case .switchInline:
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingShareIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage
case .payment:
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPaymentIconImage : graphics.chatBubbleActionButtonOutgoingPaymentIconImage
default:
iconImage = nil
}

View File

@ -21,6 +21,7 @@ import RadialStatusNode
import TelegramUIPreferences
import PeerInfoAvatarListNode
import AnimationUI
import ContextUI
enum PeerInfoHeaderButtonKey: Hashable {
case message
@ -771,6 +772,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
final class PeerInfoAvatarListNode: ASDisplayNode {
private let isSettings: Bool
let pinchSourceNode: PinchSourceContainerNode
let avatarContainerNode: PeerInfoAvatarTransformContainerNode
let listContainerTransformNode: ASDisplayNode
let listContainerNode: PeerInfoAvatarListContainerNode
@ -781,10 +783,13 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
var item: PeerInfoAvatarListItem?
var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)?
var animateOverlaysFadeIn: (() -> Void)?
init(context: AccountContext, readyWhenGalleryLoads: Bool, isSettings: Bool) {
self.isSettings = isSettings
self.pinchSourceNode = PinchSourceContainerNode()
self.avatarContainerNode = PeerInfoAvatarTransformContainerNode(context: context)
self.listContainerTransformNode = ASDisplayNode()
self.listContainerNode = PeerInfoAvatarListContainerNode(context: context)
@ -793,9 +798,10 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
super.init()
self.addSubnode(self.avatarContainerNode)
self.addSubnode(self.pinchSourceNode)
self.pinchSourceNode.contentNode.addSubnode(self.avatarContainerNode)
self.listContainerTransformNode.addSubnode(self.listContainerNode)
self.addSubnode(self.listContainerTransformNode)
self.pinchSourceNode.contentNode.addSubnode(self.listContainerTransformNode)
let avatarReady = (self.avatarContainerNode.avatarNode.ready
|> 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) {
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)
}
@ -1634,6 +1659,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
var requestOpenAvatarForEditing: ((Bool) -> Void)?
var cancelUpload: (() -> Void)?
var requestUpdateLayout: (() -> Void)?
var animateOverlaysFadeIn: (() -> Void)?
var displayAvatarContextMenu: ((ASDisplayNode, ContextGesture?) -> 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)
}
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() {

View File

@ -2390,6 +2390,13 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}
}
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
guard let strongSelf = self else {
return