From f2972aeceabe468d932308acc29507524a6ce2f3 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 9 Apr 2021 23:21:47 +0400 Subject: [PATCH 1/2] Payment shimmer and more --- submodules/BotPaymentsUI/BUILD | 1 + .../Sources/BotCheckoutActionButton.swift | 294 +++++------------- .../Sources/BotCheckoutController.swift | 65 +++- .../Sources/BotCheckoutControllerNode.swift | 140 ++++++--- .../Sources/BotCheckoutPriceItem.swift | 64 +++- .../Sources/BotReceiptControllerNode.swift | 6 +- submodules/ItemListUI/BUILD | 1 + .../Items/ItemListDisclosureItem.swift | 49 ++- .../PresentationThemeEssentialGraphics.swift | 4 + .../Message/BotPayment.imageset/Contents.json | 12 + .../Chat/Message/BotPayment.imageset/card.pdf | Bin 0 -> 3781 bytes .../TelegramUI/Sources/ChatController.swift | 8 +- .../ChatMessageActionButtonsNode.swift | 2 + 13 files changed, 370 insertions(+), 276 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/BotPayment.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/BotPayment.imageset/card.pdf diff --git a/submodules/BotPaymentsUI/BUILD b/submodules/BotPaymentsUI/BUILD index d828ebcc6f..d917b67fe4 100644 --- a/submodules/BotPaymentsUI/BUILD +++ b/submodules/BotPaymentsUI/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/AppBundle:AppBundle", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/OverlayStatusController:OverlayStatusController", + "//submodules/ShimmerEffect:ShimmerEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift index f356ce752b..e784f933b6 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift @@ -3,91 +3,37 @@ 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) 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)? + + private var placeholderNode: ShimmerEffectNode? - init(inactiveFillColor: UIColor, activeFillColor: UIColor, foregroundColor: UIColor) { - self.inactiveFillColor = inactiveFillColor + 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 @@ -99,9 +45,7 @@ final class BotCheckoutActionButton: HighlightableButtonNode { self.labelNode.isUserInteractionEnabled = false 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 - - 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))) + func updateLayout(absoluteRect: CGRect, containerSize: CGSize, transition: ContainedViewLayoutTransition) { + let size = absoluteRect.size + + self.validLayout = (absoluteRect, containerSize) + 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)) } } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift index 2b44c92e2d..c37816f33a 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift @@ -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 { + 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 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 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 } @@ -26,11 +84,14 @@ public final class BotCheckoutController: ViewController { private var presentationData: PresentationData private var didPlayPresentationAnimation = false + + private let inputData: Promise - public init(context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId) { + public init(context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId, inputData: Promise) { 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() diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index cd4e1bd811..5c3c339f5b 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -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: @@ -88,6 +104,31 @@ enum BotCheckoutEntry: ItemListNodeEntry { 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 { switch lhs { @@ -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, 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 diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift index aa374a19f4..2e33d49e95 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift @@ -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 } @@ -89,6 +92,9 @@ class BotCheckoutPriceItemNode: ListViewItemNode { let backgroundNode: ASDisplayNode let separatorNode: ASDisplayNode let bottomSeparatorNode: ASDisplayNode + + private var placeholderNode: ShimmerEffectNode? + private var absoluteLocation: (CGRect, CGSize)? private var item: BotCheckoutPriceItem? @@ -111,6 +117,15 @@ class BotCheckoutPriceItemNode: ListViewItemNode { self.addSubnode(self.separatorNode) 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) @@ -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() + } } }) } diff --git a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift index 00c478c777..f605cf78dd 100644 --- a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift @@ -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 diff --git a/submodules/ItemListUI/BUILD b/submodules/ItemListUI/BUILD index 68433a99c3..9a38bf8d03 100644 --- a/submodules/ItemListUI/BUILD +++ b/submodules/ItemListUI/BUILD @@ -22,6 +22,7 @@ swift_library( "//submodules/SegmentedControlNode:SegmentedControlNode", "//submodules/AccountContext:AccountContext", "//submodules/AnimationUI:AnimationUI", + "//submodules/ShimmerEffect:ShimmerEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index 585b750ae1..1c58b50e54 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -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?, (ListViewItemApply) -> Void)) -> Void) { @@ -131,6 +134,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { public var tag: ItemListItemTag? { return self.item?.tag } + + private var placeholderNode: ShimmerEffectNode? + private var absoluteLocation: (CGRect, CGSize)? public init() { self.backgroundNode = ASDisplayNode() @@ -179,6 +185,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) @@ -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() + } } }) } diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift index 66b3464aab..bbd74bbbcb 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift @@ -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)) diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/BotPayment.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotPayment.imageset/Contents.json new file mode 100644 index 0000000000..8d74185285 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotPayment.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "card.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/BotPayment.imageset/card.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotPayment.imageset/card.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3e219e9e059200a44a55c319c490c29d4e7c5571 GIT binary patch literal 3781 zcmai%c|4ST_s1<$7?R2osjg&3$}*d&DZ613Wot3kX2xJ_Q)BGOk|m7n4GKw-T~UdK z?8IZpmI{d>At|>t`Hk+o=kEF4&+qlSUf1iozOVB==Um@&KGz@b12M%KD8p5dU`Qk9 zD`z_I+nuh)W-tR zkh6nEH({&9%wM%eOcUuUdZ`FwY*?3};B=15^@JU(?gmg~yKzAC(2-1a@CvcyM!mwu z<@=*D0@P27hkq`NMJ65$sP`W9hke?tP=iPwP*bu=<=KhO5wAvD&xc^z|0}$l&DDfkCIaz=0|r)9<06db%ZVFe=+p z{PT33&CXrzjZ1vH#dt+6d0&h=I)ZS^SiauxERD3Z2Pd1$7eA{6J5;|_sx^gr_HP6w z$Q{*_-^v5l??bC!g-UVwk}b-cq!AmSmkTM1hzK}BGF#|wk=$5gsoP@S#F6T@R3>yh zTL#}L{T!9as5Tu=hm7VhK-o5_koQhGR|LqcvxezBoTWoOJb4d8=D{%WG23@l?2P)#i*Analw^U$^j74B!5?FT%|(vGIe5f^f{tjGHHK zmaEE zlFk_kbF1Uq881M;14f0Y!1$CdZj%j0Ug94Hc*25Dit*p%S2@W?(FQ%z(R|GV)`7p~ z^}gnEiKjQjzYBCG6n$x{4d}v)ZI^^af^pj5h&m$}?{El3F!YO<#9f_&c*%R)({%gA z_E-t}>FJmr6X$Qzi;5RZ)L!X=KG=QuT0yp+sm8)Vx=fXz?X}fA0$l)V>pE{>*p5`b z9>TWQJWqlL%TCA&;_BPqV|^uV3rM|qJ9?^8Qd9J4z0~NQFFZ=&_Vr1E+O2~8-3FF| zd*-pa@s{$uI}W%e$lc@1*H4cZy|?FC?8$`7NmES=`#-W4gvb!GHRljVmLtO{e`&t89(HjV zX)b-Ak;JHG#C=0eJY$cs-=a$?b{o$XQ&tXp9KMnwm*SYhn-W)Ge2&^D-j`9}USQG- zbAh@LTn1g%`cw*I*?DZAOm>DnYGeF+XlX?#kONtdaSk(N0p!$dW! zNvC_Hx21E^Cs8dG&Q`*YK~@~A$;WoEl(f$sXB{EsF_`S)lG!}vZq0i`g&M`A2j!Md zIcLf{ciASYSRd7SYV*{>N7uwi)l${I)NC$W(=4kv>s~Fc);j3ri!R{J#TcDibq}(m zvug>Ngq%^*!GeUs?!!4&IVYNrk)B+Rp4S|+hL%NT7$zFVww<;$??0jazt1o#xf>Da!Ar%X;#TXd0J^dUJ<{=(!JtS z*=oa^#&lO(QeaDj%NVJ=Gk0j>adE!#h8*}do9ri@uluXdash@JFqbZ;DbtueS>xpri`(e?yfOiOZJ(xePq*zP*B z^g_ag(x-__#5tF_Jk8^pQJS%uyK1~^3IkP_3G008-m6332EI(L4y>7gj)97VK7#Lo z3_#}vG=&jDJGXd&8tQoKszdjyzOt9M>6PTu{jTR7t}dh|Vk@!gxm!gERRo>!t32m> z)z=54Ow`BfHCCUDYkk#D3W1`p@iWWpNY=n_g;_ezlMzG zC&wq}C?fN_^Z!K&t5~k+(S;DA%Z}J0H56E?4X7NLUe!xV-{*6??rL`@7$5S)J65OO z_+k9VNrwC_$Thrhxyz$rCmcEJrCG<0iG}g>agX-BZQt6~uBthz`FJ_ic>jp_UZ?ii zRkS#~zr^P4vok)nbycH()J(^@1MVjWzqA{)$CCGqctj8Pn%yxgtGVc0J~z><`%Kqt zl%?EhE$Y_fCgC#jsh{{l)uhKJUE+ zeclaCReiHeBjW7_yGq{M7IQkj-u?Qt`oc`jd4l!ji`yx24sjKT2ai~%N?mY$d7tim z3XzFRpJ06ocsiT$rseHG?TC9&4{t1glYr(vaoOiVtxKk^#X9aygmt| zg6xB^vnS^h*tl@q{g(PTuf7w#14kbmO-9$pCN@7DyWB!ufB%PGz^=k6Q)JGwWzew$ znE4Ma%lqdtMbroVq5})26xOpApLAJ8TV?mZ>yLVJ4^0U$4j7pV?#bBOxmQ{1*2a|u z#$sA}`Aa7wc0#Ab?OT{EpQ~SWo5Sa0qNE~^U!ZGNRgAyNC@Cu$ZVzFW+RZdpb?))bN8E%xeFl`=rCi@g5(_Ty9?UaAA$sV#s>-8fl>= zVQR)?*(4<^R?Fv#(D$#tPgnkcJjo8gCaanQw!iRq zlb3(t@83A>38Gagho{NFBrAJLaab_Bx!3>fzB3qY%>A=Lm!;Fk@7=C*0`0z7`%kZ?7w z0RL%&!4TYz|1TR7^%z) zcRHd{>AEP)ETKh`xyd_() + inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, messageId: message.id) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + }) + strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, messageId: messageId, inputData: inputData), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } } diff --git a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift index ab5189f89b..81baa28bb1 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift @@ -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 } From da4a9977d00d5bd8e5eafdb225c24142049eecf6 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 9 Apr 2021 23:54:12 +0400 Subject: [PATCH 2/2] Pinch to zoom avatars --- .../ContextUI/Sources/PinchController.swift | 3 ++ .../Sources/PeerInfo/PeerInfoHeaderNode.swift | 43 +++++++++++++++++-- .../Sources/PeerInfo/PeerInfoScreen.swift | 7 +++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/submodules/ContextUI/Sources/PinchController.swift b/submodules/ContextUI/Sources/PinchController.swift index 22def4a138..214e88da3c 100644 --- a/submodules/ContextUI/Sources/PinchController.swift +++ b/submodules/ContextUI/Sources/PinchController.swift @@ -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() } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index fdd453ac39..fb9a716dae 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -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,9 +783,12 @@ 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() @@ -792,10 +797,11 @@ final class PeerInfoAvatarListNode: ASDisplayNode { self.listContainerNode.isHidden = true 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 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() { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 1dedd90da5..a6b36c8e90 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -2389,6 +2389,13 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD 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 guard let strongSelf = self else {