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

This commit is contained in:
Ilya Laktyushin 2022-06-01 11:10:16 +04:00
commit 76ff4e5dd9
16 changed files with 404 additions and 88 deletions

View File

@ -7677,3 +7677,5 @@ Sorry for the inconvenience.";
"Premium.Limits.AccountsInfo" = "Connect 4 accounts with different mobile numbers"; "Premium.Limits.AccountsInfo" = "Connect 4 accounts with different mobile numbers";
"WebApp.Settings" = "Settings"; "WebApp.Settings" = "Settings";
"Bot.AccepRecurrentInfo" = "I accept [Terms of Service]() of **%1$@**";

View File

@ -25,6 +25,9 @@ swift_library(
"//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/OverlayStatusController:OverlayStatusController", "//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/ShimmerEffect:ShimmerEffect", "//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/CheckNode:CheckNode",
"//submodules/TextFormat:TextFormat",
"//submodules/Markdown:Markdown",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -6,8 +6,8 @@ import PassKit
import ShimmerEffect import ShimmerEffect
enum BotCheckoutActionButtonState: Equatable { enum BotCheckoutActionButtonState: Equatable {
case active(String) case active(text: String, isEnabled: Bool)
case applePay case applePay(isEnabled: Bool)
case placeholder case placeholder
} }
@ -17,6 +17,7 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
static var height: CGFloat = 52.0 static var height: CGFloat = 52.0
private var activeFillColor: UIColor private var activeFillColor: UIColor
private var inactiveFillColor: UIColor
private var foregroundColor: UIColor private var foregroundColor: UIColor
private let activeBackgroundNode: ASImageNode private let activeBackgroundNode: ASImageNode
@ -28,17 +29,23 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
private var placeholderNode: ShimmerEffectNode? private var placeholderNode: ShimmerEffectNode?
init(activeFillColor: UIColor, foregroundColor: UIColor) { private var activeImage: UIImage?
private var inactiveImage: UIImage?
init(activeFillColor: UIColor, inactiveFillColor: UIColor, foregroundColor: UIColor) {
self.activeFillColor = activeFillColor self.activeFillColor = activeFillColor
self.inactiveFillColor = inactiveFillColor
self.foregroundColor = foregroundColor self.foregroundColor = foregroundColor
let diameter: CGFloat = 20.0 let diameter: CGFloat = 20.0
self.activeImage = generateStretchableFilledCircleImage(diameter: diameter, color: activeFillColor)
self.inactiveImage = generateStretchableFilledCircleImage(diameter: diameter, color: inactiveFillColor)
self.activeBackgroundNode = ASImageNode() self.activeBackgroundNode = ASImageNode()
self.activeBackgroundNode.displaysAsynchronously = false self.activeBackgroundNode.displaysAsynchronously = false
self.activeBackgroundNode.displayWithoutProcessing = true self.activeBackgroundNode.displayWithoutProcessing = true
self.activeBackgroundNode.isLayerBacked = true self.activeBackgroundNode.isLayerBacked = true
self.activeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: diameter, color: activeFillColor) self.activeBackgroundNode.image = self.activeImage
self.labelNode = TextNode() self.labelNode = TextNode()
self.labelNode.displaysAsynchronously = false self.labelNode.displaysAsynchronously = false
@ -75,7 +82,7 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
var labelSize = self.labelNode.bounds.size var labelSize = self.labelNode.bounds.size
if let state = self.state { if let state = self.state {
switch state { switch state {
case let .active(title): case let .active(title, isEnabled):
if let applePayButton = self.applePayButton { if let applePayButton = self.applePayButton {
self.applePayButton = nil self.applePayButton = nil
applePayButton.removeFromSuperview() applePayButton.removeFromSuperview()
@ -86,11 +93,19 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
placeholderNode.removeFromSupernode() placeholderNode.removeFromSupernode()
} }
let image = isEnabled ? self.activeImage : self.inactiveImage
if let image = image, let currentImage = self.activeBackgroundNode.image, currentImage !== image {
self.activeBackgroundNode.image = image
self.activeBackgroundNode.layer.animate(from: currentImage.cgImage! as AnyObject, to: image.cgImage! as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2)
} else {
self.activeBackgroundNode.image = image
}
let makeLayout = TextNode.asyncLayout(self.labelNode) 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 (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() let _ = labelApply()
labelSize = labelLayout.size labelSize = labelLayout.size
case .applePay: case let .applePay(isEnabled):
if self.applePayButton == nil { if self.applePayButton == nil {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
let applePayButton: PKPaymentButton let applePayButton: PKPaymentButton
@ -102,6 +117,7 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
applePayButton.addTarget(self, action: #selector(self.applePayButtonPressed), for: .touchUpInside) applePayButton.addTarget(self, action: #selector(self.applePayButtonPressed), for: .touchUpInside)
self.view.addSubview(applePayButton) self.view.addSubview(applePayButton)
self.applePayButton = applePayButton self.applePayButton = applePayButton
applePayButton.isEnabled = isEnabled
} }
} }

View File

@ -15,13 +15,16 @@ public final class BotCheckoutController: ViewController {
let form: BotPaymentForm let form: BotPaymentForm
let validatedFormInfo: BotPaymentValidatedFormInfo? let validatedFormInfo: BotPaymentValidatedFormInfo?
let botPeer: EnginePeer?
private init( private init(
form: BotPaymentForm, form: BotPaymentForm,
validatedFormInfo: BotPaymentValidatedFormInfo? validatedFormInfo: BotPaymentValidatedFormInfo?,
botPeer: EnginePeer?
) { ) {
self.form = form self.form = form
self.validatedFormInfo = validatedFormInfo self.validatedFormInfo = validatedFormInfo
self.botPeer = botPeer
} }
public static func fetch(context: AccountContext, source: BotPaymentInvoiceSource) -> Signal<InputData, FetchError> { public static func fetch(context: AccountContext, source: BotPaymentInvoiceSource) -> Signal<InputData, FetchError> {
@ -39,6 +42,9 @@ public final class BotCheckoutController: ViewController {
return .generic return .generic
} }
|> mapToSignal { paymentForm -> Signal<InputData, FetchError> in |> mapToSignal { paymentForm -> Signal<InputData, FetchError> in
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: paymentForm.paymentBotId))
|> castError(FetchError.self)
|> mapToSignal { botPeer -> Signal<InputData, FetchError> in
if let current = paymentForm.savedInfo { if let current = paymentForm.savedInfo {
return context.engine.payments.validateBotPaymentForm(saveInfo: true, source: source, formInfo: current) return context.engine.payments.validateBotPaymentForm(saveInfo: true, source: source, formInfo: current)
|> mapError { _ -> FetchError in |> mapError { _ -> FetchError in
@ -47,24 +53,28 @@ public final class BotCheckoutController: ViewController {
|> map { result -> InputData in |> map { result -> InputData in
return InputData( return InputData(
form: paymentForm, form: paymentForm,
validatedFormInfo: result validatedFormInfo: result,
botPeer: botPeer
) )
} }
|> `catch` { _ -> Signal<InputData, FetchError> in |> `catch` { _ -> Signal<InputData, FetchError> in
return .single(InputData( return .single(InputData(
form: paymentForm, form: paymentForm,
validatedFormInfo: nil validatedFormInfo: nil,
botPeer: botPeer
)) ))
} }
} else { } else {
return .single(InputData( return .single(InputData(
form: paymentForm, form: paymentForm,
validatedFormInfo: nil validatedFormInfo: nil,
botPeer: botPeer
)) ))
} }
} }
} }
} }
}
private var controllerNode: BotCheckoutControllerNode { private var controllerNode: BotCheckoutControllerNode {
return self.displayNode as! BotCheckoutControllerNode return self.displayNode as! BotCheckoutControllerNode

View File

@ -17,6 +17,9 @@ import PasswordSetupUI
import Stripe import Stripe
import LocalAuth import LocalAuth
import OverlayStatusController import OverlayStatusController
import CheckNode
import TextFormat
import Markdown
final class BotCheckoutControllerArguments { final class BotCheckoutControllerArguments {
fileprivate let account: Account fileprivate let account: Account
@ -489,6 +492,160 @@ private func availablePaymentMethods(form: BotPaymentForm, current: BotCheckoutP
return methods return methods
} }
private final class RecurrentConfirmationNode: ASDisplayNode {
private let isAcceptedUpdated: (Bool) -> Void
private let openTerms: () -> Void
private var checkNode: InteractiveCheckNode?
private let textNode: ImmediateTextNode
init(isAcceptedUpdated: @escaping (Bool) -> Void, openTerms: @escaping () -> Void) {
self.isAcceptedUpdated = isAcceptedUpdated
self.openTerms = openTerms
self.textNode = ImmediateTextNode()
self.textNode.maximumNumberOfLines = 0
super.init()
self.textNode.highlightAttributeAction = { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
}
self.textNode.tapAttributeAction = { [weak self] attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
self?.openTerms()
}
}
self.addSubnode(self.textNode)
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
guard let checkNode = self.checkNode else {
return
}
if case .ended = recognizer.state {
checkNode.setSelected(!checkNode.selected, animated: true)
checkNode.valueChanged?(checkNode.selected)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if let (_, attributes) = self.textNode.attributesAtPoint(self.view.convert(point, to: self.textNode.view)) {
if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] == nil {
return self.view
}
}
return super.hitTest(point, with: event)
}
func update(presentationData: PresentationData, botName: String, width: CGFloat, sideInset: CGFloat) -> CGFloat {
let spacing: CGFloat = 16.0
let topInset: CGFloat = 8.0
let checkNode: InteractiveCheckNode
if let current = self.checkNode {
checkNode = current
} else {
checkNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: presentationData.theme.list.itemCheckColors.fillColor, strokeColor: presentationData.theme.list.itemCheckColors.foregroundColor, borderColor: presentationData.theme.list.itemCheckColors.strokeColor, overlayBorder: false, hasInset: false, hasShadow: false))
checkNode.valueChanged = { [weak self] value in
self?.isAcceptedUpdated(value)
}
self.checkNode = checkNode
self.addSubnode(checkNode)
}
let checkSize = CGSize(width: 22.0, height: 22.0)
self.textNode.linkHighlightColor = presentationData.theme.list.itemAccentColor.withAlphaComponent(0.3)
let attributedText = parseMarkdownIntoAttributedString(
presentationData.strings.Bot_AccepRecurrentInfo(botName).string,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: presentationData.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: presentationData.theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: presentationData.theme.list.itemAccentColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
)
self.textNode.attributedText = attributedText
let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0 - spacing - checkSize.width, height: .greatestFiniteMagnitude))
let height = textSize.height + 15.0
let contentWidth = checkSize.width + spacing + textSize.width
let contentOriginX = sideInset + floor((width - sideInset * 2.0 - contentWidth) / 2.0)
checkNode.frame = CGRect(origin: CGPoint(x: contentOriginX, y: topInset + floor((height - checkSize.height) / 2.0)), size: checkSize)
self.textNode.frame = CGRect(origin: CGPoint(x: contentOriginX + checkSize.width + spacing, y: topInset + floor((height - textSize.height) / 2.0)), size: textSize)
return height
}
}
private final class ActionButtonPanelNode: ASDisplayNode {
private(set) var isAccepted: Bool = false
var isAcceptedUpdated: (() -> Void)?
var openRecurrentTerms: (() -> Void)?
private var recurrentConfirmationNode: RecurrentConfirmationNode?
func update(presentationData: PresentationData, layout: ContainerViewLayout, invoice: BotPaymentInvoice?, botName: String?) -> (CGFloat, CGFloat) {
let bottomPanelVerticalInset: CGFloat = 16.0
var height = max(layout.intrinsicInsets.bottom, layout.inputHeight ?? 0.0) + bottomPanelVerticalInset * 2.0 + BotCheckoutActionButton.height
var actionButtonOffset: CGFloat = bottomPanelVerticalInset
if let invoice = invoice, let recurrentInfo = invoice.recurrentInfo, let botName = botName {
let recurrentConfirmationNode: RecurrentConfirmationNode
if let current = self.recurrentConfirmationNode {
recurrentConfirmationNode = current
} else {
recurrentConfirmationNode = RecurrentConfirmationNode(isAcceptedUpdated: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.isAccepted = value
strongSelf.isAcceptedUpdated?()
}, openTerms: { [weak self] in
self?.openRecurrentTerms?()
})
self.recurrentConfirmationNode = recurrentConfirmationNode
self.addSubnode(recurrentConfirmationNode)
}
let _ = recurrentInfo
let recurrentConfirmationHeight = recurrentConfirmationNode.update(presentationData: presentationData, botName: botName, width: layout.size.width, sideInset: layout.safeInsets.left + 33.0)
recurrentConfirmationNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: recurrentConfirmationHeight))
actionButtonOffset += recurrentConfirmationHeight
} else if let recurrentConfirmationNode = self.recurrentConfirmationNode {
self.recurrentConfirmationNode = nil
recurrentConfirmationNode.removeFromSupernode()
}
height += actionButtonOffset - bottomPanelVerticalInset
return (height, actionButtonOffset)
}
}
final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthorizationViewControllerDelegate { final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthorizationViewControllerDelegate {
private weak var controller: BotCheckoutController? private weak var controller: BotCheckoutController?
private let navigationBar: NavigationBar private let navigationBar: NavigationBar
@ -509,6 +666,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
private let paymentFormAndInfo = Promise<(BotPaymentForm, BotPaymentRequestedInfo, BotPaymentValidatedFormInfo?, String?, BotCheckoutPaymentMethod?, Int64?)?>(nil) private let paymentFormAndInfo = Promise<(BotPaymentForm, BotPaymentRequestedInfo, BotPaymentValidatedFormInfo?, String?, BotCheckoutPaymentMethod?, Int64?)?>(nil)
private var paymentFormValue: BotPaymentForm? private var paymentFormValue: BotPaymentForm?
private var botPeerValue: EnginePeer?
private var currentFormInfo: BotPaymentRequestedInfo? private var currentFormInfo: BotPaymentRequestedInfo?
private var currentValidatedFormInfo: BotPaymentValidatedFormInfo? private var currentValidatedFormInfo: BotPaymentValidatedFormInfo?
private var currentShippingOptionId: String? private var currentShippingOptionId: String?
@ -516,7 +674,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
private var currentTipAmount: Int64? private var currentTipAmount: Int64?
private var formRequestDisposable: Disposable? private var formRequestDisposable: Disposable?
private let actionButtonPanelNode: ASDisplayNode private let actionButtonPanelNode: ActionButtonPanelNode
private let actionButtonPanelSeparator: ASDisplayNode private let actionButtonPanelSeparator: ASDisplayNode
private let actionButton: BotCheckoutActionButton private let actionButton: BotCheckoutActionButton
private let inProgressDimNode: ASDisplayNode private let inProgressDimNode: ASDisplayNode
@ -585,13 +743,11 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
return (ItemListPresentationData(presentationData), (nodeState, arguments)) return (ItemListPresentationData(presentationData), (nodeState, arguments))
} }
self.actionButtonPanelNode = ASDisplayNode() self.actionButtonPanelNode = ActionButtonPanelNode()
self.actionButtonPanelNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor
self.actionButtonPanelSeparator = ASDisplayNode() self.actionButtonPanelSeparator = ASDisplayNode()
self.actionButtonPanelSeparator.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.actionButton = BotCheckoutActionButton(activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor) self.actionButton = BotCheckoutActionButton(activeFillColor: self.presentationData.theme.list.itemAccentColor, inactiveFillColor: self.presentationData.theme.list.itemDisabledTextColor.mixedWith(self.presentationData.theme.list.blocksBackgroundColor, alpha: 0.7), foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor)
self.actionButton.setState(.placeholder) self.actionButton.setState(.placeholder)
self.inProgressDimNode = ASDisplayNode() self.inProgressDimNode = ASDisplayNode()
@ -603,6 +759,22 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
self.arguments = arguments self.arguments = arguments
self.actionButtonPanelNode.isAcceptedUpdated = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateActionButton()
}
self.actionButtonPanelNode.openRecurrentTerms = { [weak self] in
guard let strongSelf = self, let paymentForm = strongSelf.paymentFormValue, let recurrentInfo = paymentForm.invoice.recurrentInfo else {
return
}
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: recurrentInfo.termsUrl, forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {
self?.view.endEditing(true)
})
}
openInfoImpl = { [weak self] focus in openInfoImpl = { [weak self] focus in
if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo { if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo {
strongSelf.controller?.view.endEditing(true) strongSelf.controller?.view.endEditing(true)
@ -924,6 +1096,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
savedInfo = BotPaymentRequestedInfo(name: nil, phone: nil, email: nil, shippingAddress: nil) savedInfo = BotPaymentRequestedInfo(name: nil, phone: nil, email: nil, shippingAddress: nil)
} }
strongSelf.paymentFormValue = formAndValidatedInfo.form strongSelf.paymentFormValue = formAndValidatedInfo.form
strongSelf.botPeerValue = formAndValidatedInfo.botPeer
strongSelf.currentFormInfo = savedInfo strongSelf.currentFormInfo = savedInfo
strongSelf.currentValidatedFormInfo = formAndValidatedInfo.validatedFormInfo strongSelf.currentValidatedFormInfo = formAndValidatedInfo.validatedFormInfo
if let savedCredentials = formAndValidatedInfo.form.savedCredentials { if let savedCredentials = formAndValidatedInfo.form.savedCredentials {
@ -959,6 +1132,38 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
} }
} }
}) })
self.actionButtonPanelSeparator.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.actionButtonPanelNode.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor
self.visibleBottomContentOffsetChanged = { [weak self] offset in
guard let strongSelf = self else {
return
}
let panelColor: UIColor
let separatorColor: UIColor
switch offset {
case let .known(value):
if value > 10.0 {
panelColor = strongSelf.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor
separatorColor = strongSelf.presentationData.theme.rootController.navigationBar.separatorColor
} else {
panelColor = .clear
separatorColor = .clear
}
default:
panelColor = strongSelf.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor
separatorColor = strongSelf.presentationData.theme.rootController.navigationBar.separatorColor
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear)
if strongSelf.actionButtonPanelNode.backgroundColor != panelColor {
transition.updateBackgroundColor(node: strongSelf.actionButtonPanelNode, color: panelColor)
}
if strongSelf.actionButtonPanelSeparator.backgroundColor != separatorColor {
transition.updateBackgroundColor(node: strongSelf.actionButtonPanelSeparator, color: separatorColor)
}
}
} }
deinit { deinit {
@ -971,22 +1176,34 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
private func updateActionButton() { private func updateActionButton() {
let totalAmount = currentTotalPrice(paymentForm: self.paymentFormValue, validatedFormInfo: self.currentValidatedFormInfo, currentShippingOptionId: self.currentShippingOptionId, currentTip: self.currentTipAmount) let totalAmount = currentTotalPrice(paymentForm: self.paymentFormValue, validatedFormInfo: self.currentValidatedFormInfo, currentShippingOptionId: self.currentShippingOptionId, currentTip: self.currentTipAmount)
let payString: String let payString: String
var isButtonEnabled = true
if let paymentForm = self.paymentFormValue, totalAmount > 0 { if let paymentForm = self.paymentFormValue, totalAmount > 0 {
payString = self.presentationData.strings.Checkout_PayPrice(formatCurrencyAmount(totalAmount, currency: paymentForm.invoice.currency)).string payString = self.presentationData.strings.Checkout_PayPrice(formatCurrencyAmount(totalAmount, currency: paymentForm.invoice.currency)).string
if let _ = paymentForm.invoice.recurrentInfo {
if !self.actionButtonPanelNode.isAccepted {
isButtonEnabled = false
}
}
} else { } else {
payString = self.presentationData.strings.CheckoutInfo_Pay payString = self.presentationData.strings.CheckoutInfo_Pay
} }
self.actionButton.isEnabled = isButtonEnabled
if let currentPaymentMethod = self.currentPaymentMethod { if let currentPaymentMethod = self.currentPaymentMethod {
switch currentPaymentMethod { switch currentPaymentMethod {
case .applePay: case .applePay:
self.actionButton.setState(.applePay) self.actionButton.setState(.applePay(isEnabled: isButtonEnabled))
default: default:
self.actionButton.setState(.active(payString)) self.actionButton.setState(.active(text: payString, isEnabled: isButtonEnabled))
} }
} else { } else {
self.actionButton.setState(.active(payString)) self.actionButton.setState(.active(text: payString, isEnabled: isButtonEnabled))
} }
self.actionButtonPanelNode.isHidden = false self.actionButtonPanelNode.isHidden = false
self.controller?.requestLayout(transition: .immediate)
} }
private func updateIsInProgress(_ value: Bool) { private func updateIsInProgress(_ value: Bool) {
@ -1006,13 +1223,18 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
var updatedInsets = layout.intrinsicInsets var updatedInsets = layout.intrinsicInsets
let bottomPanelHorizontalInset: CGFloat = 16.0 let bottomPanelHorizontalInset: CGFloat = 16.0
let bottomPanelVerticalInset: CGFloat = 16.0
let bottomPanelHeight = max(updatedInsets.bottom, layout.inputHeight ?? 0.0) + bottomPanelVerticalInset * 2.0 + BotCheckoutActionButton.height var botName: String?
if let botPeer = self.botPeerValue {
botName = botPeer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)
}
let (bottomPanelHeight, actionButtonOffset) = self.actionButtonPanelNode.update(presentationData: self.presentationData, layout: layout, invoice: self.paymentFormValue?.invoice, botName: botName)
transition.updateFrame(node: self.actionButtonPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelHeight), size: CGSize(width: layout.size.width, height: bottomPanelHeight))) transition.updateFrame(node: self.actionButtonPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelHeight), size: CGSize(width: layout.size.width, height: bottomPanelHeight)))
transition.updateFrame(node: self.actionButtonPanelSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.actionButtonPanelSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
let actionButtonFrame = CGRect(origin: CGPoint(x: bottomPanelHorizontalInset, y: bottomPanelVerticalInset), size: CGSize(width: layout.size.width - bottomPanelHorizontalInset * 2.0, height: BotCheckoutActionButton.height)) let actionButtonFrame = CGRect(origin: CGPoint(x: bottomPanelHorizontalInset, y: actionButtonOffset), size: CGSize(width: layout.size.width - bottomPanelHorizontalInset * 2.0, height: BotCheckoutActionButton.height))
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
self.actionButton.updateLayout(absoluteRect: actionButtonFrame.offsetBy(dx: self.actionButtonPanelNode.frame.minX, dy: self.actionButtonPanelNode.frame.minY), containerSize: layout.size, transition: transition) self.actionButton.updateLayout(absoluteRect: actionButtonFrame.offsetBy(dx: self.actionButtonPanelNode.frame.minX, dy: self.actionButtonPanelNode.frame.minY), containerSize: layout.size, transition: transition)

View File

@ -305,8 +305,8 @@ final class BotReceiptControllerNode: ItemListControllerNode {
self.actionButtonPanelSeparator = ASDisplayNode() self.actionButtonPanelSeparator = ASDisplayNode()
self.actionButtonPanelSeparator.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.actionButtonPanelSeparator.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.actionButton = BotCheckoutActionButton(activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.plainBackgroundColor) self.actionButton = BotCheckoutActionButton(activeFillColor: self.presentationData.theme.list.itemAccentColor, inactiveFillColor: self.presentationData.theme.list.itemDisabledTextColor, foregroundColor: self.presentationData.theme.list.plainBackgroundColor)
self.actionButton.setState(.active(self.presentationData.strings.Common_Done)) self.actionButton.setState(.active(text: self.presentationData.strings.Common_Done, isEnabled: true))
super.init(controller: controller, navigationBar: navigationBar, state: signal) super.init(controller: controller, navigationBar: navigationBar, state: signal)

View File

@ -171,7 +171,7 @@ public struct EnvironmentBuilder {
} }
public static func buildExpression<T: Equatable>(_ expression: EnvironmentValue<T>) -> Partial<T> { public static func buildExpression<T: Equatable>(_ expression: EnvironmentValue<T>) -> Partial<T> {
return Partial<T>(value: expression) return Partial<T>(value: EnvironmentValue(expression.value))
} }
public static func buildBlock<T1: Equatable>(_ t1: Partial<T1>) -> Environment<T1> { public static func buildBlock<T1: Equatable>(_ t1: Partial<T1>) -> Environment<T1> {

View File

@ -4239,9 +4239,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
if itemNode.apparentFrame.maxY <= visualInsets.top { if itemNode.apparentFrame.maxY <= visualInsets.top {
offsetRanges.offset(IndexRange(first: 0, last: index), offset: -apparentHeightDelta) offsetRanges.offset(IndexRange(first: 0, last: index), offset: -apparentHeightDelta)
} else if invertOffsetDirection { } else if invertOffsetDirection /*&& itemNode.frame.height < self.visibleSize.height*/ {
if itemNode.apparentFrame.minY - apparentHeightDelta < visualInsets.top { if self.scroller.contentOffset.y < 1.0 {
let overflowOffset = visualInsets.top - (itemNode.apparentFrame.minY - apparentHeightDelta) /*let overflowOffset = visualInsets.top - (itemNode.apparentFrame.minY - apparentHeightDelta)
let remainingOffset = apparentHeightDelta - overflowOffset let remainingOffset = apparentHeightDelta - overflowOffset
offsetRanges.offset(IndexRange(first: 0, last: index), offset: -remainingOffset) offsetRanges.offset(IndexRange(first: 0, last: index), offset: -remainingOffset)
@ -4255,6 +4255,18 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
} }
} }
offsetRanges.offset(IndexRange(first: index + 1, last: Int.max), offset: offsetDelta)*/
var offsetDelta = apparentHeightDelta
if offsetDelta < 0.0 {
let maxDelta = visualInsets.top - itemNode.apparentFrame.maxY
if maxDelta > offsetDelta {
let remainingOffset = maxDelta - offsetDelta
offsetRanges.offset(IndexRange(first: 0, last: index), offset: remainingOffset)
offsetDelta = maxDelta
}
}
offsetRanges.offset(IndexRange(first: index + 1, last: Int.max), offset: offsetDelta) offsetRanges.offset(IndexRange(first: index + 1, last: Int.max), offset: offsetDelta)
} else { } else {
offsetRanges.offset(IndexRange(first: 0, last: index), offset: -apparentHeightDelta) offsetRanges.offset(IndexRange(first: 0, last: index), offset: -apparentHeightDelta)

View File

@ -812,7 +812,7 @@ struct ListViewState {
for node in self.nodes { for node in self.nodes {
i += 1 i += 1
if node.index == itemIndex { if node.index == itemIndex {
if isAnimated { if !isAnimated {
let offsetDirection: ListViewInsertionOffsetDirection let offsetDirection: ListViewInsertionOffsetDirection
if let direction = direction { if let direction = direction {
offsetDirection = ListViewInsertionOffsetDirection(direction) offsetDirection = ListViewInsertionOffsetDirection(direction)

View File

@ -1345,36 +1345,54 @@ public func standaloneStateManager(
removeDatabaseOnError: false removeDatabaseOnError: false
) )
Logger.shared.log("StandaloneStateManager", "Prepare request postbox")
return postbox return postbox
|> take(1) |> take(1)
|> mapToSignal { result -> Signal<AccountStateManager?, NoError> in |> mapToSignal { result -> Signal<AccountStateManager?, NoError> in
switch result { switch result {
case .upgrading: case .upgrading:
Logger.shared.log("StandaloneStateManager", "Received postbox: upgrading")
return .single(nil) return .single(nil)
case .error: case .error:
Logger.shared.log("StandaloneStateManager", "Received postbox: error")
return .single(nil) return .single(nil)
case let .postbox(postbox): case let .postbox(postbox):
Logger.shared.log("StandaloneStateManager", "Received postbox: valid")
return accountManager.transaction { transaction -> (LocalizationSettings?, ProxySettings?) in return accountManager.transaction { transaction -> (LocalizationSettings?, ProxySettings?) in
return (nil, transaction.getSharedData(SharedDataKeys.proxySettings)?.get(ProxySettings.self)) return (nil, transaction.getSharedData(SharedDataKeys.proxySettings)?.get(ProxySettings.self))
} }
|> mapToSignal { localizationSettings, proxySettings -> Signal<AccountStateManager?, NoError> in |> mapToSignal { localizationSettings, proxySettings -> Signal<AccountStateManager?, NoError> in
Logger.shared.log("StandaloneStateManager", "Received settings")
return postbox.transaction { transaction -> (PostboxCoding?, LocalizationSettings?, ProxySettings?, NetworkSettings?) in return postbox.transaction { transaction -> (PostboxCoding?, LocalizationSettings?, ProxySettings?, NetworkSettings?) in
let state = transaction.getState() let state = transaction.getState()
return (state, localizationSettings, proxySettings, transaction.getPreferencesEntry(key: PreferencesKeys.networkSettings)?.get(NetworkSettings.self)) return (state, localizationSettings, proxySettings, transaction.getPreferencesEntry(key: PreferencesKeys.networkSettings)?.get(NetworkSettings.self))
} }
|> mapToSignal { accountState, localizationSettings, proxySettings, networkSettings -> Signal<AccountStateManager?, NoError> in |> mapToSignal { accountState, localizationSettings, proxySettings, networkSettings -> Signal<AccountStateManager?, NoError> in
Logger.shared.log("StandaloneStateManager", "Received state")
let keychain = makeExclusiveKeychain(id: id, postbox: postbox) let keychain = makeExclusiveKeychain(id: id, postbox: postbox)
if let accountState = accountState { if let accountState = accountState {
switch accountState { switch accountState {
case _ as UnauthorizedAccountState: case _ as UnauthorizedAccountState:
Logger.shared.log("StandaloneStateManager", "state is UnauthorizedAccountState")
return .single(nil) return .single(nil)
case let authorizedState as AuthorizedAccountState: case let authorizedState as AuthorizedAccountState:
Logger.shared.log("StandaloneStateManager", "state is valid")
return postbox.transaction { transaction -> String? in return postbox.transaction { transaction -> String? in
return (transaction.getPeer(authorizedState.peerId) as? TelegramUser)?.phone return (transaction.getPeer(authorizedState.peerId) as? TelegramUser)?.phone
} }
|> mapToSignal { phoneNumber in |> mapToSignal { phoneNumber in
Logger.shared.log("StandaloneStateManager", "received phone number")
return initializedNetwork( return initializedNetwork(
accountId: id, accountId: id,
arguments: networkArguments, arguments: networkArguments,
@ -1389,6 +1407,8 @@ public func standaloneStateManager(
phoneNumber: phoneNumber phoneNumber: phoneNumber
) )
|> map { network -> AccountStateManager? in |> map { network -> AccountStateManager? in
Logger.shared.log("StandaloneStateManager", "received network")
return AccountStateManager( return AccountStateManager(
accountPeerId: authorizedState.peerId, accountPeerId: authorizedState.peerId,
accountManager: accountManager, accountManager: accountManager,
@ -1404,6 +1424,8 @@ public func standaloneStateManager(
} }
} }
default: default:
Logger.shared.log("StandaloneStateManager", "Unexpected accountState")
assertionFailure("Unexpected accountState \(accountState)") assertionFailure("Unexpected accountState \(accountState)")
return .single(nil) return .single(nil)
} }

View File

@ -1102,8 +1102,8 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo
} }
} }
case let .updateTranscribeAudio(flags, transcriptionId, text): case let .updateTranscribeAudio(flags, transcriptionId, text):
let isPending = (flags & (1 << 0)) != 0 let isFinal = (flags & (1 << 0)) != 0
updatedState.updateAudioTranscription(id: transcriptionId, isPending: isPending, text: text) updatedState.updateAudioTranscription(id: transcriptionId, isPending: !isFinal, text: text)
case let .updateNotifySettings(apiPeer, apiNotificationSettings): case let .updateNotifySettings(apiPeer, apiNotificationSettings):
switch apiPeer { switch apiPeer {
case let .notifyPeer(peer): case let .notifyPeer(peer):

View File

@ -45,11 +45,16 @@ public struct BotPaymentInvoice : Equatable {
public var suggested: [Int64] public var suggested: [Int64]
} }
public struct RecurrentInfo: Equatable {
public var termsUrl: String
}
public let isTest: Bool public let isTest: Bool
public let requestedFields: BotPaymentInvoiceFields public let requestedFields: BotPaymentInvoiceFields
public let currency: String public let currency: String
public let prices: [BotPaymentPrice] public let prices: [BotPaymentPrice]
public let tip: Tip? public let tip: Tip?
public let recurrentInfo: RecurrentInfo?
} }
public struct BotPaymentNativeProvider : Equatable { public struct BotPaymentNativeProvider : Equatable {
@ -124,7 +129,7 @@ public enum BotPaymentFormRequestError {
extension BotPaymentInvoice { extension BotPaymentInvoice {
init(apiInvoice: Api.Invoice) { init(apiInvoice: Api.Invoice) {
switch apiInvoice { switch apiInvoice {
case let .invoice(flags, currency, prices, maxTipAmount, suggestedTipAmounts, _): case let .invoice(flags, currency, prices, maxTipAmount, suggestedTipAmounts, recurrentTermsUrl):
var fields = BotPaymentInvoiceFields() var fields = BotPaymentInvoiceFields()
if (flags & (1 << 1)) != 0 { if (flags & (1 << 1)) != 0 {
fields.insert(.name) fields.insert(.name)
@ -147,6 +152,10 @@ extension BotPaymentInvoice {
if (flags & (1 << 7)) != 0 { if (flags & (1 << 7)) != 0 {
fields.insert(.emailAvailableToProvider) fields.insert(.emailAvailableToProvider)
} }
var recurrentInfo: BotPaymentInvoice.RecurrentInfo?
if let recurrentTermsUrl = recurrentTermsUrl {
recurrentInfo = BotPaymentInvoice.RecurrentInfo(termsUrl: recurrentTermsUrl)
}
var parsedTip: BotPaymentInvoice.Tip? var parsedTip: BotPaymentInvoice.Tip?
if let maxTipAmount = maxTipAmount, let suggestedTipAmounts = suggestedTipAmounts { if let maxTipAmount = maxTipAmount, let suggestedTipAmounts = suggestedTipAmounts {
parsedTip = BotPaymentInvoice.Tip(max: maxTipAmount, suggested: suggestedTipAmounts) parsedTip = BotPaymentInvoice.Tip(max: maxTipAmount, suggested: suggestedTipAmounts)
@ -156,7 +165,7 @@ extension BotPaymentInvoice {
case let .labeledPrice(label, amount): case let .labeledPrice(label, amount):
return BotPaymentPrice(label: label, amount: amount) return BotPaymentPrice(label: label, amount: amount)
} }
}, tip: parsedTip) }, tip: parsedTip, recurrentInfo: recurrentInfo)
} }
} }
} }

View File

@ -235,6 +235,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
var openMedia: ((InteractiveMediaNodeActivateContent) -> Void)? var openMedia: ((InteractiveMediaNodeActivateContent) -> Void)?
var activateAction: (() -> Void)? var activateAction: (() -> Void)?
var requestUpdateLayout: (() -> Void)?
var visibility: ListViewItemNodeVisibility = .none { var visibility: ListViewItemNodeVisibility = .none {
didSet { didSet {
@ -837,7 +838,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
strongSelf.theme = presentationData.theme strongSelf.theme = presentationData.theme
strongSelf.lineNode.image = lineImage strongSelf.lineNode.image = lineImage
strongSelf.lineNode.frame = CGRect(origin: CGPoint(x: 13.0, y: insets.top), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0)) animation.animator.updateFrame(layer: strongSelf.lineNode.layer, frame: CGRect(origin: CGPoint(x: 13.0, y: insets.top), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0)), completion: nil)
strongSelf.lineNode.isHidden = !displayLine strongSelf.lineNode.isHidden = !displayLine
strongSelf.textNode.displaysAsynchronously = !isPreview strongSelf.textNode.displaysAsynchronously = !isPreview
@ -931,6 +932,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
strongSelf.openMedia?(.default) strongSelf.openMedia?(.default)
} }
} }
contentFileNode.requestUpdateLayout = { [weak strongSelf] _ in
if let strongSelf = strongSelf {
strongSelf.requestUpdateLayout?()
}
}
} }
if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentFileSize) contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentFileSize)

View File

@ -41,11 +41,15 @@ private func transcribedText(message: Message) -> TranscribedText? {
if let attribute = attribute as? AudioTranscriptionMessageAttribute { if let attribute = attribute as? AudioTranscriptionMessageAttribute {
if !attribute.text.isEmpty { if !attribute.text.isEmpty {
return .success(text: attribute.text, isPending: attribute.isPending) return .success(text: attribute.text, isPending: attribute.isPending)
} else {
if attribute.isPending {
return nil
} else { } else {
return .error return .error
} }
} }
} }
}
return nil return nil
} }
@ -345,6 +349,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
var shouldBeginTranscription = false var shouldBeginTranscription = false
var shouldExpandNow = false var shouldExpandNow = false
if case .expanded = self.audioTranscriptionState {
shouldExpandNow = true
} else {
if let result = transcribedText(message: message) { if let result = transcribedText(message: message) {
shouldExpandNow = true shouldExpandNow = true
@ -356,6 +364,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
} else { } else {
shouldBeginTranscription = true shouldBeginTranscription = true
} }
}
if shouldBeginTranscription { if shouldBeginTranscription {
if self.transcribeDisposable == nil { if self.transcribeDisposable == nil {

View File

@ -96,6 +96,11 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
} }
} }
} }
self.contentNode.requestUpdateLayout = { [weak self] in
if let strongSelf = self, let item = strongSelf.item {
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id)
}
}
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {

View File

@ -667,7 +667,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.textInputBackgroundNode.isUserInteractionEnabled = true self.textInputBackgroundNode.isUserInteractionEnabled = true
self.textInputBackgroundNode.view.addGestureRecognizer(recognizer) self.textInputBackgroundNode.view.addGestureRecognizer(recognizer)
if let presentationContext = presentationContext { /*if let presentationContext = presentationContext {
self.emojiViewProvider = { [weak self, weak presentationContext] emoji in self.emojiViewProvider = { [weak self, weak presentationContext] emoji in
guard let strongSelf = self, let presentationContext = presentationContext, let presentationInterfaceState = strongSelf.presentationInterfaceState, let context = strongSelf.context, let file = strongSelf.context?.animatedEmojiStickers[emoji]?.first?.file else { guard let strongSelf = self, let presentationContext = presentationContext, let presentationInterfaceState = strongSelf.presentationInterfaceState, let context = strongSelf.context, let file = strongSelf.context?.animatedEmojiStickers[emoji]?.first?.file else {
return UIView() return UIView()
@ -675,7 +675,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
return EmojiTextAttachmentView(context: context, file: file, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12)) return EmojiTextAttachmentView(context: context, file: file, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12))
} }
} }*/
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {