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

# Conflicts:
#	submodules/TelegramCore/Sources/BotPaymentForm.swift
This commit is contained in:
overtake 2021-04-09 11:41:23 +04:00
commit adc3f4b44a
72 changed files with 6016 additions and 5170 deletions

View File

@ -48,9 +48,11 @@ jobs:
cd $SOURCE_DIR
BUILD_NUMBER_OFFSET="$(cat build_number_offset)"
export APP_VERSION=$(cat versions.json | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["app"]);')
export COMMIT_COUNT=$(git rev-list --count HEAD)
export COMMIT_COUNT="$(($COMMIT_COUNT+2000))"
export COMMIT_COUNT="$(($COMMIT_COUNT+$BUILD_NUMBER_OFFSET))"
export BUILD_NUMBER="$COMMIT_COUNT"
echo "BUILD_NUMBER=$(echo $BUILD_NUMBER)" >> $GITHUB_ENV
echo "APP_VERSION=$(echo $APP_VERSION)" >> $GITHUB_ENV

View File

@ -70,6 +70,7 @@ beta_testflight:
stage: build
only:
- beta
- hotfix
except:
- tags
script:
@ -87,6 +88,7 @@ deploy_beta_testflight:
stage: deploy
only:
- beta
- hotfix
except:
- tags
script:
@ -100,6 +102,7 @@ verifysanity_beta_testflight:
stage: verifysanity
only:
- beta
- hotfix
except:
- tags
script:
@ -118,6 +121,7 @@ verify_beta_testflight:
stage: verify
only:
- beta
- hotfix
except:
- tags
script:

View File

@ -1 +1 @@
6098b6ed7c06e42f7bb7226e92744e7951c4c3f89787d702280f907e68a60a15
6eb592f57eca2cd3cda976727ba368ed

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -6340,11 +6340,15 @@ Sorry for the inconvenience.";
"VoiceChat.PinVideo" = "Pin Video";
"VoiceChat.UnpinVideo" = "Unpin Video";
"Notification.VoiceChatScheduled" = "Voice chat scheduled for %@";
"Notification.VoiceChatScheduledChannel" = "Voice chat scheduled for %@";
"Notification.VoiceChatScheduled" = "%1$@ Voice chat scheduled for %2$@";
"VoiceChat.StartsIn" = "Starts in";
"VoiceChat.LateBy" = "Late by";
"VoiceChat.StatusStartsIn" = "starts in %@";
"VoiceChat.StatusLateBy" = "late by %@";
"VoiceChat.StartNow" = "Start Now";
"VoiceChat.SetReminder" = "Set Reminder";
"VoiceChat.CancelReminder" = "Cancel Reminder";

1
build_number_offset Normal file
View File

@ -0,0 +1 @@
2100

View File

@ -79,7 +79,8 @@ COMMIT_ID="$(git rev-parse HEAD)"
COMMIT_AUTHOR=$(git log -1 --pretty=format:'%an')
if [ -z "$2" ]; then
COMMIT_COUNT=$(git rev-list --count HEAD)
COMMIT_COUNT="$(($COMMIT_COUNT+2000))"
BUILD_NUMBER_OFFSET="$(cat build_number_offset)"
COMMIT_COUNT="$(($COMMIT_COUNT+$BUILD_NUMBER_OFFSET))"
BUILD_NUMBER="$COMMIT_COUNT"
else
BUILD_NUMBER="$2"

View File

@ -43,7 +43,7 @@ enum BotCheckoutActionButtonState: Equatable {
private let titleFont = Font.semibold(17.0)
final class BotCheckoutActionButton: HighlightableButtonNode {
static var diameter: CGFloat = 48.0
static var height: CGFloat = 52.0
private var inactiveFillColor: UIColor
private var activeFillColor: UIColor
@ -62,12 +62,14 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
self.inactiveFillColor = inactiveFillColor
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: BotCheckoutActionButton.diameter, height: BotCheckoutActionButton.diameter), rotatedContext: { size, context in
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)
@ -75,7 +77,7 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
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 = 10.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)))
})
@ -83,14 +85,14 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
self.inactiveBackgroundNode.displaysAsynchronously = false
self.inactiveBackgroundNode.displayWithoutProcessing = true
self.inactiveBackgroundNode.isLayerBacked = true
self.inactiveBackgroundNode.image = generateStretchableFilledCircleImage(diameter: BotCheckoutActionButton.diameter, color: self.foregroundColor, strokeColor: activeFillColor, strokeWidth: 2.0)
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
self.activeBackgroundNode.isLayerBacked = true
self.activeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: BotCheckoutActionButton.diameter, color: activeFillColor)
self.activeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: diameter, color: activeFillColor)
self.labelNode = TextNode()
self.labelNode.displaysAsynchronously = false
@ -178,10 +180,21 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
case .applePay:
if case .applePay = previousState {
} else {
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 {
@ -226,15 +239,19 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
}
}
}
@objc private func applePayButtonPressed() {
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.diameter) / 2.0), y: 0.0), size: CGSize(width: BotCheckoutActionButton.diameter, height: BotCheckoutActionButton.diameter)))
transition.updateFrame(node: self.inactiveBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.diameter)))
transition.updateFrame(node: self.activeBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.diameter)))
transition.updateFrame(node: self.progressBackgroundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - BotCheckoutActionButton.height) / 2.0), y: 0.0), size: CGSize(width: BotCheckoutActionButton.height, height: BotCheckoutActionButton.height)))
transition.updateFrame(node: self.inactiveBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.height)))
transition.updateFrame(node: self.activeBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.height)))
if let applePayButton = self.applePayButton {
applePayButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.diameter))
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

View File

@ -55,7 +55,7 @@ enum BotCheckoutEntry: ItemListNodeEntry {
var section: ItemListSectionId {
switch self {
case .header:
return BotCheckoutSection.header.rawValue
return BotCheckoutSection.prices.rawValue
case .price, .tip:
return BotCheckoutSection.prices.rawValue
default:
@ -286,7 +286,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, false))
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false, index == 0))
totalPrice += price.amount
index += 1
}
@ -447,7 +447,9 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
private var currentPaymentMethod: BotCheckoutPaymentMethod?
private var currentTipAmount: Int64?
private var formRequestDisposable: Disposable?
private let actionButtonPanelNode: ASDisplayNode
private let actionButtonPanelSeparator: ASDisplayNode
private let actionButton: BotCheckoutActionButton
private let inProgressDimNode: ASDisplayNode
@ -481,13 +483,20 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
let signal: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError> = combineLatest(context.sharedContext.presentationData, self.state.get(), paymentFormAndInfo.get(), context.account.postbox.loadedPeerWithId(messageId.peerId))
|> map { presentationData, state, paymentFormAndInfo, botPeer -> (ItemListPresentationData, (ItemListNodeState, Any)) in
let nodeState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: botCheckoutControllerEntries(presentationData: presentationData, state: state, invoice: invoice, paymentForm: paymentFormAndInfo?.0, formInfo: paymentFormAndInfo?.1, validatedFormInfo: paymentFormAndInfo?.2, currentShippingOptionId: paymentFormAndInfo?.3, currentPaymentMethod: paymentFormAndInfo?.4, currentTip: paymentFormAndInfo?.5, botPeer: botPeer), style: .plain, focusItemTag: nil, emptyStateItem: nil, animateChanges: false)
let nodeState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: botCheckoutControllerEntries(presentationData: presentationData, state: state, invoice: invoice, paymentForm: paymentFormAndInfo?.0, formInfo: paymentFormAndInfo?.1, validatedFormInfo: paymentFormAndInfo?.2, currentShippingOptionId: paymentFormAndInfo?.3, currentPaymentMethod: paymentFormAndInfo?.4, currentTip: paymentFormAndInfo?.5, botPeer: botPeer), style: .blocks, focusItemTag: nil, emptyStateItem: nil, animateChanges: false)
return (ItemListPresentationData(presentationData), (nodeState, arguments))
}
self.actionButtonPanelNode = ASDisplayNode()
self.actionButtonPanelNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor
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(.loading)
self.actionButton.setState(.active(""))
self.actionButtonPanelNode.isHidden = true
self.inProgressDimNode = ASDisplayNode()
self.inProgressDimNode.alpha = 0.0
@ -522,6 +531,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo {
strongSelf.currentPaymentMethod = method
strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount)))
strongSelf.updateActionButton()
}
}
@ -551,7 +561,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
var dismissImpl: (() -> Void)?
let canSave = paymentForm.canSaveCredentials || paymentForm.passwordMissing
let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, additionalFields: additionalFields, publishableKey: publishableKey, completion: { method in
let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, provider: .stripe(additionalFields: additionalFields, publishableKey: publishableKey), completion: { method in
guard let strongSelf = self else {
return
}
@ -606,6 +616,74 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
controller?.dismiss()
}
strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} else if let nativeProvider = paymentForm.nativeProvider, nativeProvider.name == "smartglocal" {
guard let paramsData = nativeProvider.params.data(using: .utf8) else {
return
}
guard let nativeParams = (try? JSONSerialization.jsonObject(with: paramsData)) as? [String: Any] else {
return
}
guard let publicToken = nativeParams["public_token"] as? String else {
return
}
var dismissImpl: (() -> Void)?
let canSave = paymentForm.canSaveCredentials || paymentForm.passwordMissing
let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, provider: .smartglobal(isTesting: paymentForm.invoice.isTest, publicToken: publicToken), completion: { method in
guard let strongSelf = self else {
return
}
if canSave && paymentForm.passwordMissing {
switch method {
case let .webToken(webToken) where webToken.saveOnServer:
var text = strongSelf.presentationData.strings.Checkout_NewCard_SaveInfoEnableHelp
text = text.replacingOccurrences(of: "[", with: "")
text = text.replacingOccurrences(of: "]", with: "")
present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_NotNow, action: {
var updatedToken = webToken
updatedToken.saveOnServer = false
applyPaymentMethod(.webToken(updatedToken))
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: {
guard let strongSelf = self else {
return
}
if paymentForm.passwordMissing {
var updatedToken = webToken
updatedToken.saveOnServer = false
applyPaymentMethod(.webToken(updatedToken))
let controller = SetupTwoStepVerificationController(context: strongSelf.context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in
if shouldDismiss {
controller.dismiss()
}
switch update {
case .noPassword, .awaitingEmailConfirmation:
break
case .passwordSet:
var updatedToken = webToken
updatedToken.saveOnServer = true
applyPaymentMethod(.webToken(updatedToken))
}
})
strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} else {
var updatedToken = webToken
updatedToken.saveOnServer = true
applyPaymentMethod(.webToken(updatedToken))
}
})]), nil)
default:
applyPaymentMethod(method)
}
} else {
applyPaymentMethod(method)
}
dismissImpl?()
})
dismissImpl = { [weak controller] in
controller?.dismiss()
}
strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} else {
var dismissImpl: (() -> Void)?
let controller = BotCheckoutWebInteractionController(context: context, url: paymentForm.url, intent: .addPaymentMethod({ [weak self] token in
@ -753,10 +831,13 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
}, error: { _ in
})
self.addSubnode(self.actionButtonPanelNode)
self.actionButtonPanelNode.addSubnode(self.actionButtonPanelSeparator)
self.actionButtonPanelNode.addSubnode(self.actionButton)
self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside)
self.actionButton.isEnabled = false
self.addSubnode(self.actionButton)
self.listNode.supernode?.insertSubnode(self.inProgressDimNode, aboveSubnode: self.listNode)
}
@ -775,21 +856,36 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
} else {
payString = self.presentationData.strings.CheckoutInfo_Pay
}
if self.actionButton.isEnabled {
self.actionButton.setState(.active(payString))
if let currentPaymentMethod = self.currentPaymentMethod {
switch currentPaymentMethod {
case .applePay:
self.actionButton.setState(.applePay)
default:
self.actionButton.setState(.active(payString))
}
} else {
self.actionButton.setState(.loading)
self.actionButton.setState(.active(payString))
}
self.actionButtonPanelNode.isHidden = false
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) {
var updatedInsets = layout.intrinsicInsets
updatedInsets.bottom += BotCheckoutActionButton.diameter + 20.0
super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition, additionalInsets: additionalInsets)
let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter))
let bottomPanelHorizontalInset: CGFloat = 16.0
let bottomPanelVerticalInset: CGFloat = 16.0
let bottomPanelHeight = updatedInsets.bottom + bottomPanelVerticalInset * 2.0 + BotCheckoutActionButton.height
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)))
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)
updatedInsets.bottom = bottomPanelHeight
super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition, additionalInsets: additionalInsets)
transition.updateFrame(node: self.inProgressDimNode, frame: self.listNode.frame)
}

View File

@ -80,7 +80,6 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
@ -109,7 +108,8 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
self.highlightedBackgroundNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.imageNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
@ -209,9 +209,9 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
}
strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: contentInsets.left, y: contentInsets.top), size: imageSize)
if strongSelf.backgroundNode.supernode != nil {
/*if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
}*/
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
@ -231,7 +231,8 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
strongSelf.textNode.frame = textFrame
strongSelf.botNameNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + textBotNameSpacing), size: botNameLayout.size)
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1000.0), size: CGSize(width: params.width, height: contentSize.height + 1000.0))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel))
}
})

View File

@ -30,13 +30,17 @@ struct BotCheckoutNativeCardEntryAdditionalFields: OptionSet {
}
final class BotCheckoutNativeCardEntryController: ViewController {
enum Provider {
case stripe(additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String)
case smartglobal(isTesting: Bool, publicToken: String)
}
private var controllerNode: BotCheckoutNativeCardEntryControllerNode {
return super.displayNode as! BotCheckoutNativeCardEntryControllerNode
}
private let context: AccountContext
private let additionalFields: BotCheckoutNativeCardEntryAdditionalFields
private let publishableKey: String
private let provider: Provider
private let completion: (BotCheckoutPaymentMethod) -> Void
private var presentationData: PresentationData
@ -46,10 +50,9 @@ final class BotCheckoutNativeCardEntryController: ViewController {
private var doneItem: UIBarButtonItem?
private var activityItem: UIBarButtonItem?
public init(context: AccountContext, additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String, completion: @escaping (BotCheckoutPaymentMethod) -> Void) {
public init(context: AccountContext, provider: Provider, completion: @escaping (BotCheckoutPaymentMethod) -> Void) {
self.context = context
self.additionalFields = additionalFields
self.publishableKey = publishableKey
self.provider = provider
self.completion = completion
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -71,7 +74,7 @@ final class BotCheckoutNativeCardEntryController: ViewController {
}
override public func loadDisplayNode() {
self.displayNode = BotCheckoutNativeCardEntryControllerNode(additionalFields: self.additionalFields, publishableKey: self.publishableKey, theme: self.presentationData.theme, strings: self.presentationData.strings, present: { [weak self] c, a in
self.displayNode = BotCheckoutNativeCardEntryControllerNode(provider: self.provider, theme: self.presentationData.theme, strings: self.presentationData.strings, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}, dismiss: { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)

View File

@ -42,7 +42,7 @@ private final class BotCheckoutNativeCardEntryScrollerNode: ASDisplayNode {
}
final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, UIScrollViewDelegate {
private let publishableKey: String
private let provider: BotCheckoutNativeCardEntryController.Provider
private let present: (ViewController, Any?) -> Void
private let dismiss: () -> Void
@ -70,9 +70,11 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
private var currentCardData: BotPaymentCardInputData?
private var currentCountryIso2: String?
private var dataTask: URLSessionDataTask?
init(additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String, theme: PresentationTheme, strings: PresentationStrings, present: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void, openCountrySelection: @escaping () -> Void, updateStatus: @escaping (BotCheckoutNativeCardEntryStatus) -> Void, completion: @escaping (BotCheckoutPaymentMethod) -> Void) {
self.publishableKey = publishableKey
init(provider: BotCheckoutNativeCardEntryController.Provider, theme: PresentationTheme, strings: PresentationStrings, present: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void, openCountrySelection: @escaping () -> Void, updateStatus: @escaping (BotCheckoutNativeCardEntryStatus) -> Void, completion: @escaping (BotCheckoutPaymentMethod) -> Void) {
self.provider = provider
self.present = present
self.dismiss = dismiss
@ -95,46 +97,53 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
cardUpdatedImpl?(data)
}
itemNodes.append([BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PaymentCard), self.cardItem])
if additionalFields.contains(.cardholderName) {
var sectionItems: [BotPaymentItemNode] = []
sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_CardholderNameTitle))
let cardholderItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_CardholderNamePlaceholder, contentType: .name)
self.cardholderItem = cardholderItem
sectionItems.append(cardholderItem)
itemNodes.append(sectionItems)
} else {
self.cardholderItem = nil
}
if additionalFields.contains(.country) || additionalFields.contains(.zipCode) {
var sectionItems: [BotPaymentItemNode] = []
sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PostcodeTitle))
if additionalFields.contains(.country) {
let countryItem = BotPaymentDisclosureItemNode(title: "", placeholder: strings.CheckoutInfo_ShippingInfoCountryPlaceholder, text: "")
countryItem.action = {
openCountrySelectionImpl?()
switch provider {
case let .stripe(additionalFields, _):
if additionalFields.contains(.cardholderName) {
var sectionItems: [BotPaymentItemNode] = []
sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_CardholderNameTitle))
let cardholderItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_CardholderNamePlaceholder, contentType: .name)
self.cardholderItem = cardholderItem
sectionItems.append(cardholderItem)
itemNodes.append(sectionItems)
} else {
self.cardholderItem = nil
}
if additionalFields.contains(.country) || additionalFields.contains(.zipCode) {
var sectionItems: [BotPaymentItemNode] = []
sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PostcodeTitle))
if additionalFields.contains(.country) {
let countryItem = BotPaymentDisclosureItemNode(title: "", placeholder: strings.CheckoutInfo_ShippingInfoCountryPlaceholder, text: "")
countryItem.action = {
openCountrySelectionImpl?()
}
self.countryItem = countryItem
sectionItems.append(countryItem)
} else {
self.countryItem = nil
}
self.countryItem = countryItem
sectionItems.append(countryItem)
if additionalFields.contains(.zipCode) {
let zipCodeItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_PostcodePlaceholder, contentType: .address)
self.zipCodeItem = zipCodeItem
sectionItems.append(zipCodeItem)
} else {
self.zipCodeItem = nil
}
itemNodes.append(sectionItems)
} else {
self.countryItem = nil
}
if additionalFields.contains(.zipCode) {
let zipCodeItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_PostcodePlaceholder, contentType: .address)
self.zipCodeItem = zipCodeItem
sectionItems.append(zipCodeItem)
} else {
self.zipCodeItem = nil
}
itemNodes.append(sectionItems)
} else {
case .smartglobal:
self.cardholderItem = nil
self.countryItem = nil
self.zipCodeItem = nil
}
@ -214,6 +223,7 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
deinit {
self.verifyDisposable.dispose()
self.dataTask?.cancel()
}
func updateCountry(_ iso2: String) {
@ -232,53 +242,149 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
guard let cardData = self.currentCardData else {
return
}
let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration
configuration.smsAutofillDisabled = true
configuration.publishableKey = self.publishableKey
configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph"
let apiClient = STPAPIClient(configuration: configuration)
let card = STPCardParams()
card.number = cardData.number
card.cvc = cardData.code
card.expYear = cardData.year
card.expMonth = cardData.month
card.name = self.cardholderItem?.text
card.addressCountry = self.currentCountryIso2
card.addressZip = self.zipCodeItem?.text
let createToken: Signal<STPToken, Error> = Signal { subscriber in
apiClient.createToken(withCard: card, completion: { token, error in
if let error = error {
subscriber.putError(error)
} else if let token = token {
subscriber.putNext(token)
subscriber.putCompletion()
switch self.provider {
case let .stripe(_, publishableKey):
let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration
configuration.smsAutofillDisabled = true
configuration.publishableKey = publishableKey
configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph"
let apiClient = STPAPIClient(configuration: configuration)
let card = STPCardParams()
card.number = cardData.number
card.cvc = cardData.code
card.expYear = cardData.year
card.expMonth = cardData.month
card.name = self.cardholderItem?.text
card.addressCountry = self.currentCountryIso2
card.addressZip = self.zipCodeItem?.text
let createToken: Signal<STPToken, Error> = Signal { subscriber in
apiClient.createToken(withCard: card, completion: { token, error in
if let error = error {
subscriber.putError(error)
} else if let token = token {
subscriber.putNext(token)
subscriber.putCompletion()
}
})
return ActionDisposable {
let _ = apiClient.publishableKey
}
}
self.isVerifying = true
self.verifyDisposable.set((createToken |> deliverOnMainQueue).start(next: { [weak self] token in
if let strongSelf = self, let card = token.card {
let last4 = card.last4()
let brand = STPAPIClient.string(with: card.brand)
strongSelf.completion(.webToken(BotCheckoutPaymentWebToken(title: "\(brand)*\(last4)", data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: strongSelf.saveInfoItem.isOn)))
}
}, error: { [weak self] error in
if let strongSelf = self {
strongSelf.isVerifying = false
strongSelf.updateDone()
}
}))
self.updateDone()
case let .smartglobal(isTesting, publicToken):
let url: String
if isTesting {
url = "https://tgb-playground.smart-glocal.com/cds/v1/tokenize/card"
} else {
url = "https://tgb.smart-glocal.com/cds/v1/tokenize/card"
}
let jsonPayload: [String: Any] = [
"card": [
"number": cardData.number,
"expiration_month": "\(cardData.month)",
"expiration_year": "\(cardData.year)",
"security_code": "\(cardData.code)"
] as [String: Any]
]
guard let parsedUrl = URL(string: url) else {
return
}
var request = URLRequest(url: parsedUrl)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(publicToken, forHTTPHeaderField: "X-PUBLIC-TOKEN")
guard let requestBody = try? JSONSerialization.data(withJSONObject: jsonPayload, options: []) else {
return
}
request.httpBody = requestBody
let session = URLSession.shared
let dataTask = session.dataTask(with: request, completionHandler: { [weak self] data, response, error in
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
enum ReponseError: Error {
case generic
}
do {
guard let data = data else {
throw ReponseError.generic
}
let jsonRaw = try JSONSerialization.jsonObject(with: data, options: [])
guard let json = jsonRaw as? [String: Any] else {
throw ReponseError.generic
}
guard let resultData = json["data"] as? [String: Any] else {
throw ReponseError.generic
}
guard let resultInfo = resultData["info"] as? [String: Any] else {
throw ReponseError.generic
}
guard let token = resultData["token"] as? String else {
throw ReponseError.generic
}
guard let maskedCardNumber = resultInfo["masked_card_number"] as? String else {
throw ReponseError.generic
}
let responseJson: [String: Any] = [
"type": "card",
"id": "\(token)"
]
let serializedResponseJson = try JSONSerialization.data(withJSONObject: responseJson, options: [])
guard let serializedResponseString = String(data: serializedResponseJson, encoding: .utf8) else {
throw ReponseError.generic
}
strongSelf.completion(.webToken(BotCheckoutPaymentWebToken(
title: maskedCardNumber,
data: serializedResponseString,
saveOnServer: strongSelf.saveInfoItem.isOn
)))
} catch {
strongSelf.isVerifying = false
strongSelf.updateDone()
}
}
})
return ActionDisposable {
let _ = apiClient.publishableKey
}
self.dataTask = dataTask
self.isVerifying = true
self.updateDone()
dataTask.resume()
break
}
self.isVerifying = true
self.verifyDisposable.set((createToken |> deliverOnMainQueue).start(next: { [weak self] token in
if let strongSelf = self, let card = token.card {
let last4 = card.last4()
let brand = STPAPIClient.string(with: card.brand)
strongSelf.completion(.webToken(BotCheckoutPaymentWebToken(title: "\(brand)*\(last4)", data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: strongSelf.saveInfoItem.isOn)))
}
}, error: { [weak self] error in
if let strongSelf = self {
strongSelf.isVerifying = false
strongSelf.updateDone()
}
}))
self.updateDone()
}
private func updateDone() {

View File

@ -29,7 +29,7 @@ class BotCheckoutPriceItem: ListViewItem, ItemListItem {
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = BotCheckoutPriceItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), previousItem, nextItem)
node.contentSize = layout.contentSize
node.insets = layout.insets
@ -48,7 +48,7 @@ class BotCheckoutPriceItem: ListViewItem, ItemListItem {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), previousItem, nextItem)
Queue.mainQueue().async {
completion(layout, { _ in
apply()
@ -69,13 +69,13 @@ private func priceItemInsets(_ neighbors: ItemListNeighbors) -> UIEdgeInsets {
var insets = UIEdgeInsets()
switch neighbors.top {
case .otherSection:
insets.top += 8.0
insets.top += 24.0
case .none, .sameSection:
break
}
switch neighbors.bottom {
case .none, .otherSection:
insets.bottom += 8.0
insets.bottom += 24.0
case .sameSection:
break
}
@ -86,9 +86,9 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
let titleNode: TextNode
let labelNode: TextNode
let backgroundNode: ASDisplayNode
let separatorNode: ASDisplayNode
let bottomSeparatorNode: ASDisplayNode
let spacerNode: ASDisplayNode
private var item: BotCheckoutPriceItem?
@ -99,37 +99,44 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.backgroundNode = ASDisplayNode()
self.separatorNode = ASDisplayNode()
self.bottomSeparatorNode = ASDisplayNode()
self.spacerNode = ASDisplayNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.spacerNode)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.bottomSeparatorNode)
}
func asyncLayout() -> (_ item: BotCheckoutPriceItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
func asyncLayout() -> (_ item: BotCheckoutPriceItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors, _ previousItem: ListViewItem?, _ nextItem: ListViewItem?) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
return { item, params, neighbors in
return { item, params, neighbors, previousItem, nextItem in
let rightInset: CGFloat = 16.0 + params.rightInset
let naturalContentHeight: CGFloat = 34.0
var contentSize = CGSize(width: params.width, height: naturalContentHeight)
var insets = priceItemInsets(neighbors)
if item.hasSeparator {
insets.top += 5.0
}
let naturalContentHeight: CGFloat
var verticalOffset: CGFloat = 0.0
if item.isFinal {
contentSize.height += 34.0
naturalContentHeight = 44.0
} else {
naturalContentHeight = 34.0
}
if let _ = previousItem as? BotCheckoutHeaderItem {
verticalOffset += 8.0
}
var contentSize = CGSize(width: params.width, height: naturalContentHeight + verticalOffset)
if let nextItem = nextItem as? BotCheckoutPriceItem {
if nextItem.isFinal {
contentSize.height += 8.0
}
}
let insets = priceItemInsets(neighbors)
let textFont: UIFont
let textColor: UIColor
@ -154,21 +161,15 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
let leftInset: CGFloat = 16.0 + params.leftInset
strongSelf.separatorNode.isHidden = !item.hasSeparator
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
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
strongSelf.bottomSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
strongSelf.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: naturalContentHeight + 10.0), size: CGSize(width: params.width, height: UIScreenPixel))
strongSelf.bottomSeparatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: CGSize(width: params.width, height: UIScreenPixel))
strongSelf.spacerNode.isHidden = !item.isFinal
strongSelf.spacerNode.backgroundColor = item.theme.list.blocksBackgroundColor
strongSelf.spacerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: naturalContentHeight + 10.0 + UIScreenPixel), size: CGSize(width: params.width, height: max(0.0, contentSize.height - naturalContentHeight - UIScreenPixel)))
var verticalOffset: CGFloat = 0.0
if item.hasSeparator {
verticalOffset += 5.0
}
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: contentSize.height))
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)

View File

@ -122,7 +122,7 @@ private final class TipValueNode: ASDisplayNode {
func update(theme: PresentationTheme, text: String, isHighlighted: Bool, height: CGFloat) -> (CGFloat, (CGFloat) -> Void) {
var updateBackground = false
let backgroundColor = isHighlighted ? UIColor(rgb: 0x00A650) : UIColor(rgb: 0xE5F6ED)
let backgroundColor = isHighlighted ? theme.list.paymentOption.activeFillColor : theme.list.paymentOption.inactiveFillColor
if let currentBackgroundColor = self.currentBackgroundColor {
if !currentBackgroundColor.isEqual(backgroundColor) {
updateBackground = true
@ -135,7 +135,7 @@ private final class TipValueNode: ASDisplayNode {
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: backgroundColor)
}
self.titleNode.attributedText = NSAttributedString(string: text, font: Font.semibold(15.0), textColor: isHighlighted ? UIColor(rgb: 0xffffff) : UIColor(rgb: 0x00A650))
self.titleNode.attributedText = NSAttributedString(string: text, font: Font.semibold(15.0), textColor: isHighlighted ? theme.list.paymentOption.activeForegroundColor : theme.list.paymentOption.inactiveForegroundColor)
let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: height))
let minWidth: CGFloat = 80.0
@ -154,20 +154,23 @@ private final class TipValueNode: ASDisplayNode {
}
class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
private let backgroundNode: ASDisplayNode
let titleNode: TextNode
let labelNode: TextNode
let tipMeasurementNode: ImmediateTextNode
let tipCurrencyNode: ImmediateTextNode
private let textNode: TextFieldNode
private var formatterDelegate: CurrencyUITextFieldDelegate?
private let scrollNode: ASScrollNode
private var valueNodes: [TipValueNode] = []
private var item: BotCheckoutTipItem?
private var formatterDelegate: CurrencyUITextFieldDelegate?
init() {
self.backgroundNode = ASDisplayNode()
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
@ -191,6 +194,8 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
}
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode)
@ -272,7 +277,11 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
var textInputFrame = CGRect(origin: CGPoint(x: params.width - leftInset - 150.0, y: -2.0), size: CGSize(width: 150.0, height: labelsContentHeight))
var currencyText: (String, String) = formatCurrencyAmountCustom(item.numericValue, currency: item.currency)
let currencyText: (String, String, Bool) = formatCurrencyAmountCustom(item.numericValue, currency: item.currency)
let currencySymbolOnTheLeft = currencyText.2
//let currencySymbolOnTheLeft = true
if strongSelf.textNode.textField.text ?? "" != currencyText.0 {
strongSelf.textNode.textField.text = currencyText.0
strongSelf.labelNode.isHidden = !currencyText.0.isEmpty
@ -281,10 +290,16 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
strongSelf.tipMeasurementNode.attributedText = NSAttributedString(string: currencyText.0, font: titleFont, textColor: textColor)
let inputTextSize = strongSelf.tipMeasurementNode.updateLayout(textInputFrame.size)
strongSelf.tipCurrencyNode.attributedText = NSAttributedString(string: " \(currencyText.1)", font: titleFont, textColor: textColor)
let spaceRect = NSAttributedString(string: " ", font: titleFont, textColor: textColor).boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
strongSelf.tipCurrencyNode.attributedText = NSAttributedString(string: "\(currencyText.1)", font: titleFont, textColor: textColor)
let currencySize = strongSelf.tipCurrencyNode.updateLayout(CGSize(width: 100.0, height: .greatestFiniteMagnitude))
strongSelf.tipCurrencyNode.frame = CGRect(origin: CGPoint(x: textInputFrame.maxX - currencySize.width, y: floor((labelsContentHeight - currencySize.height) / 2.0) - 1.0), size: currencySize)
textInputFrame.origin.x -= currencySize.width
if currencySymbolOnTheLeft {
strongSelf.tipCurrencyNode.frame = CGRect(origin: CGPoint(x: textInputFrame.maxX - currencySize.width - inputTextSize.width - spaceRect.width, y: floor((labelsContentHeight - currencySize.height) / 2.0) - 1.0), size: currencySize)
} else {
strongSelf.tipCurrencyNode.frame = CGRect(origin: CGPoint(x: textInputFrame.maxX - currencySize.width, y: floor((labelsContentHeight - currencySize.height) / 2.0) - 1.0), size: currencySize)
textInputFrame.origin.x -= currencySize.width + spaceRect.width
}
strongSelf.textNode.frame = textInputFrame
@ -347,6 +362,9 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
strongSelf.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: valueY), size: CGSize(width: params.width, height: max(0.0, contentSize.height - valueY)))
strongSelf.scrollNode.view.contentSize = CGSize(width: variantsOffset, height: strongSelf.scrollNode.frame.height)
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: contentSize.height))
}
})
}
@ -382,7 +400,7 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
if value > item.maxValue {
value = item.maxValue
let currencyText: (String, String) = formatCurrencyAmountCustom(value, currency: item.currency)
let currencyText = formatCurrencyAmountCustom(value, currency: item.currency)
if self.textNode.textField.text ?? "" != currencyText.0 {
self.textNode.textField.text = currencyText.0
}
@ -400,6 +418,11 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
}
@objc public func textFieldDidBeginEditing(_ textField: UITextField) {
textField.selectedTextRange = textField.textRange(from: textField.endOfDocument, to: textField.endOfDocument)
}
@objc public func textFieldDidChangeSelection(_ textField: UITextField) {
textField.selectedTextRange = textField.textRange(from: textField.endOfDocument, to: textField.endOfDocument)
}
@objc public func textFieldDidEndEditing(_ textField: UITextField) {

View File

@ -117,7 +117,7 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate {
textInset = max(measuredInset, textInset)
transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: textInset, y: 3.0), size: CGSize(width: max(1.0, width - textInset - 8.0), height: 40.0)))
transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: textInset, y: 0.0), size: CGSize(width: max(1.0, width - textInset - 8.0), height: 40.0)))
return 44.0
}

View File

@ -20,16 +20,14 @@ public final class BotReceiptController: ViewController {
}
private let context: AccountContext
private let invoice: TelegramMediaInvoice
private let messageId: MessageId
private var presentationData: PresentationData
private var didPlayPresentationAnimation = false
public init(context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId) {
public init(context: AccountContext, messageId: MessageId) {
self.context = context
self.invoice = invoice
self.messageId = messageId
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -38,10 +36,10 @@ public final class BotReceiptController: ViewController {
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
var title = self.presentationData.strings.Checkout_Receipt_Title
if invoice.flags.contains(.isTest) {
let title = self.presentationData.strings.Checkout_Receipt_Title
/*if invoice.flags.contains(.isTest) {
title += " (Test)"
}
}*/
self.title = title
}
@ -54,7 +52,7 @@ public final class BotReceiptController: ViewController {
if let strongSelf = self {
strongSelf.navigationOffset = offset
}
}, context: self.context, invoice: self.invoice, messageId: self.messageId, dismissAnimated: { [weak self] in
}, context: self.context, messageId: self.messageId, dismissAnimated: { [weak self] in
self?.dismiss()
})

View File

@ -28,7 +28,7 @@ private enum BotReceiptSection: Int32 {
enum BotReceiptEntry: ItemListNodeEntry {
case header(PresentationTheme, TelegramMediaInvoice, String)
case price(Int, PresentationTheme, String, String, Bool)
case price(Int, PresentationTheme, String, String, Bool, Bool)
case paymentMethod(PresentationTheme, String, String)
case shippingInfo(PresentationTheme, String, String)
case shippingMethod(PresentationTheme, String, String)
@ -39,7 +39,7 @@ enum BotReceiptEntry: ItemListNodeEntry {
var section: ItemListSectionId {
switch self {
case .header:
return BotReceiptSection.header.rawValue
return BotReceiptSection.prices.rawValue
case .price:
return BotReceiptSection.prices.rawValue
default:
@ -51,7 +51,7 @@ enum BotReceiptEntry: ItemListNodeEntry {
switch self {
case .header:
return 0
case let .price(index, _, _, _, _):
case let .price(index, _, _, _, _, _):
return 1 + Int32(index)
case .paymentMethod:
return 10000 + 0
@ -85,8 +85,8 @@ enum BotReceiptEntry: ItemListNodeEntry {
} else {
return false
}
case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsFinal):
if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsFinal) = rhs {
case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsHasSeparator, lhsFinal):
if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsHasSeparator, rhsFinal) = rhs {
if lhsIndex != rhsIndex {
return false
}
@ -99,6 +99,9 @@ enum BotReceiptEntry: ItemListNodeEntry {
if lhsValue != rhsValue {
return false
}
if lhsHasSeparator != rhsHasSeparator {
return false
}
if lhsFinal != rhsFinal {
return false
}
@ -154,8 +157,8 @@ enum BotReceiptEntry: 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):
return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: false, 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)
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):
@ -172,21 +175,23 @@ enum BotReceiptEntry: ItemListNodeEntry {
}
}
private func botReceiptControllerEntries(presentationData: PresentationData, invoice: TelegramMediaInvoice, formInvoice: BotPaymentInvoice?, formInfo: BotPaymentRequestedInfo?, shippingOption: BotPaymentShippingOption?, paymentMethodTitle: String?, botPeer: Peer?) -> [BotReceiptEntry] {
private func botReceiptControllerEntries(presentationData: PresentationData, invoice: TelegramMediaInvoice?, formInvoice: BotPaymentInvoice?, formInfo: BotPaymentRequestedInfo?, shippingOption: BotPaymentShippingOption?, paymentMethodTitle: String?, botPeer: Peer?, tipAmount: Int64?) -> [BotReceiptEntry] {
var entries: [BotReceiptEntry] = []
var botName = ""
if let botPeer = botPeer {
botName = botPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
}
entries.append(.header(presentationData.theme, invoice, botName))
if let invoice = invoice {
entries.append(.header(presentationData.theme, invoice, botName))
}
if let formInvoice = formInvoice {
var totalPrice: Int64 = 0
var index = 0
for price in formInvoice.prices {
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: formInvoice.currency), false))
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: formInvoice.currency), index == 0, false))
totalPrice += price.amount
index += 1
}
@ -196,13 +201,20 @@ private func botReceiptControllerEntries(presentationData: PresentationData, inv
shippingOptionString = shippingOption.title
for price in shippingOption.prices {
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: formInvoice.currency), false))
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: formInvoice.currency), index == 0, false))
totalPrice += price.amount
index += 1
}
}
if let tipAmount = tipAmount, tipAmount != 0 {
//TODO:localize
entries.append(.price(index, presentationData.theme, "Tip", formatCurrencyAmount(tipAmount, currency: formInvoice.currency), index == 0, false))
totalPrice += tipAmount
index += 1
}
entries.append(.price(index, presentationData.theme, presentationData.strings.Checkout_TotalAmount, formatCurrencyAmount(totalPrice, currency: formInvoice.currency), true))
entries.append(.price(index, presentationData.theme, presentationData.strings.Checkout_TotalAmount, formatCurrencyAmount(totalPrice, currency: formInvoice.currency), true, true))
if let paymentMethodTitle = paymentMethodTitle {
entries.append(.paymentMethod(presentationData.theme, presentationData.strings.Checkout_PaymentMethod, paymentMethodTitle))
@ -262,12 +274,12 @@ final class BotReceiptControllerNode: ItemListControllerNode {
private var presentationData: PresentationData
private let receiptData = Promise<(BotPaymentInvoice, BotPaymentRequestedInfo?, BotPaymentShippingOption?, String?)?>(nil)
private let receiptData = Promise<(BotPaymentInvoice, BotPaymentRequestedInfo?, BotPaymentShippingOption?, String?, TelegramMediaInvoice, Int64?)?>(nil)
private var dataRequestDisposable: Disposable?
private let actionButton: BotCheckoutActionButton
init(controller: ItemListController?, navigationBar: NavigationBar, updateNavigationOffset: @escaping (CGFloat) -> Void, context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId, dismissAnimated: @escaping () -> Void) {
init(controller: ItemListController?, navigationBar: NavigationBar, updateNavigationOffset: @escaping (CGFloat) -> Void, context: AccountContext, messageId: MessageId, dismissAnimated: @escaping () -> Void) {
self.context = context
self.dismissAnimated = dismissAnimated
@ -277,19 +289,19 @@ final class BotReceiptControllerNode: ItemListControllerNode {
let signal: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError> = combineLatest(context.sharedContext.presentationData, receiptData.get(), context.account.postbox.loadedPeerWithId(messageId.peerId))
|> map { presentationData, receiptData, botPeer -> (ItemListPresentationData, (ItemListNodeState, Any)) in
let nodeState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: botReceiptControllerEntries(presentationData: presentationData, invoice: invoice, formInvoice: receiptData?.0, formInfo: receiptData?.1, shippingOption: receiptData?.2, paymentMethodTitle: receiptData?.3, botPeer: botPeer), style: .plain, focusItemTag: nil, emptyStateItem: nil, animateChanges: false)
let nodeState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: botReceiptControllerEntries(presentationData: presentationData, invoice: receiptData?.4, formInvoice: receiptData?.0, formInfo: receiptData?.1, shippingOption: receiptData?.2, paymentMethodTitle: receiptData?.3, botPeer: botPeer, tipAmount: receiptData?.5), style: .blocks, focusItemTag: nil, emptyStateItem: nil, animateChanges: false)
return (ItemListPresentationData(presentationData), (nodeState, arguments))
}
self.actionButton = BotCheckoutActionButton(inactiveFillColor: self.presentationData.theme.list.plainBackgroundColor, activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.plainBackgroundColor)
self.actionButton.setState(.inactive(self.presentationData.strings.Common_Done))
self.actionButton.setState(.active(self.presentationData.strings.Common_Done))
super.init(controller: controller, navigationBar: navigationBar, updateNavigationOffset: updateNavigationOffset, state: signal)
self.dataRequestDisposable = (requestBotPaymentReceipt(account: context.account, messageId: messageId) |> deliverOnMainQueue).start(next: { [weak self] receipt in
if let strongSelf = self {
strongSelf.receiptData.set(.single((receipt.invoice, receipt.info, receipt.shippingOption, receipt.credentialsTitle)))
strongSelf.receiptData.set(.single((receipt.invoice, receipt.info, receipt.shippingOption, receipt.credentialsTitle, receipt.invoiceMedia, receipt.tipAmount)))
}
})
@ -303,10 +315,11 @@ final class BotReceiptControllerNode: ItemListControllerNode {
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) {
var updatedInsets = layout.intrinsicInsets
updatedInsets.bottom += BotCheckoutActionButton.diameter + 20.0
updatedInsets.bottom += BotCheckoutActionButton.height + 16.0 * 2.0
super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition, additionalInsets: additionalInsets)
let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter))
let actionButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: layout.size.height - 16.0 - BotCheckoutActionButton.height - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width - 16.0 * 2.0, height: BotCheckoutActionButton.height))
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
self.actionButton.updateLayout(size: actionButtonFrame.size, transition: transition)
}

View File

@ -108,6 +108,12 @@ extension CurrencyUITextFieldDelegate: UITextFieldDelegate {
return false
}
public func textFieldDidChangeSelection(_ textField: UITextField) {
if #available(iOSApplicationExtension 13.0, iOS 13.0, *) {
passthroughDelegate?.textFieldDidChangeSelection?(textField)
}
}
}
// MARK: - Private

View File

@ -30,15 +30,18 @@ final class PinchSourceGesture: UIPinchGestureRecognizer {
private let target: Target
private(set) var currentTransform: (CGFloat, CGPoint)?
private(set) var currentTransform: (CGFloat, CGPoint, CGPoint)?
var began: (() -> Void)?
var updated: ((CGFloat, CGPoint) -> Void)?
var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
var ended: (() -> Void)?
private var lastLocation: CGPoint?
private var initialLocation: CGPoint?
private var pinchLocation = CGPoint()
private var currentOffset = CGPoint()
private var currentNumberOfTouches = 0
init() {
self.target = Target()
@ -52,11 +55,14 @@ final class PinchSourceGesture: UIPinchGestureRecognizer {
override func reset() {
super.reset()
self.lastLocation = nil
self.currentNumberOfTouches = 0
self.initialLocation = nil
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
//self.currentTouches.formUnion(touches)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
@ -69,40 +75,41 @@ final class PinchSourceGesture: UIPinchGestureRecognizer {
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if touches.count >= 2 {
var locationSum = CGPoint()
for touch in touches {
let point = touch.location(in: self.view)
locationSum.x += point.x
locationSum.y += point.y
}
locationSum.x /= CGFloat(touches.count)
locationSum.y /= CGFloat(touches.count)
if let lastLocation = self.lastLocation {
self.currentOffset = CGPoint(x: locationSum.x - lastLocation.x, y: locationSum.y - lastLocation.y)
} else {
self.lastLocation = locationSum
self.currentOffset = CGPoint()
}
if let (scale, _) = self.currentTransform {
self.currentTransform = (scale, self.currentOffset)
self.updated?(scale, self.currentOffset)
}
}
}
private func gestureUpdated() {
switch self.state {
case .began:
self.lastLocation = nil
self.currentOffset = CGPoint()
self.currentTransform = nil
let pinchLocation = self.location(in: self.view)
self.pinchLocation = pinchLocation
self.initialLocation = pinchLocation
let scale = max(1.0, self.scale)
self.currentTransform = (scale, self.pinchLocation, self.currentOffset)
self.currentNumberOfTouches = self.numberOfTouches
self.began?()
case .changed:
let locationSum = self.location(in: self.view)
if self.numberOfTouches < 2 && self.currentNumberOfTouches >= 2 {
self.initialLocation = CGPoint(x: locationSum.x - self.currentOffset.x, y: locationSum.y - self.currentOffset.y)
}
self.currentNumberOfTouches = self.numberOfTouches
if let initialLocation = self.initialLocation {
self.currentOffset = CGPoint(x: locationSum.x - initialLocation.x, y: locationSum.y - initialLocation.y)
}
if let (scale, pinchLocation, _) = self.currentTransform {
self.currentTransform = (scale, pinchLocation, self.currentOffset)
self.updated?(scale, pinchLocation, self.currentOffset)
}
let scale = max(1.0, self.scale)
self.currentTransform = (scale, self.currentOffset)
self.updated?(scale, self.currentOffset)
self.currentTransform = (scale, self.pinchLocation, self.currentOffset)
self.updated?(scale, self.pinchLocation, self.currentOffset)
case .ended, .cancelled:
self.ended?()
default:
@ -152,12 +159,14 @@ public final class PinchSourceContainerNode: ASDisplayNode {
}
}
public var maxPinchScale: CGFloat = 10.0
private var isActive: Bool = false
public var activate: ((PinchSourceContainerNode) -> Void)?
public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
var deactivate: (() -> Void)?
var updated: ((CGFloat, CGPoint) -> Void)?
var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
override public init() {
self.gesture = PinchSourceGesture()
@ -187,12 +196,12 @@ public final class PinchSourceContainerNode: ASDisplayNode {
strongSelf.deactivate?()
}
self.gesture.updated = { [weak self] scale, offset in
self.gesture.updated = { [weak self] scale, pinchLocation, offset in
guard let strongSelf = self else {
return
}
strongSelf.updated?(scale, offset)
strongSelf.scaleUpdated?(scale, .immediate)
strongSelf.updated?(min(scale, strongSelf.maxPinchScale), pinchLocation, offset)
strongSelf.scaleUpdated?(min(scale, strongSelf.maxPinchScale), .immediate)
}
}
@ -226,7 +235,14 @@ public final class PinchSourceContainerNode: ASDisplayNode {
private final class PinchControllerNode: ViewControllerTracingNode {
private weak var controller: PinchController?
private var initialSourceFrame: CGRect?
private let clippingNode: ASDisplayNode
private let scrollingContainer: ASDisplayNode
private let sourceNode: PinchSourceContainerNode
private let getContentAreaInScreenSpace: () -> CGRect
private let dimNode: ASDisplayNode
@ -235,17 +251,25 @@ private final class PinchControllerNode: ViewControllerTracingNode {
private var hapticFeedback: HapticFeedback?
init(controller: PinchController, sourceNode: PinchSourceContainerNode) {
init(controller: PinchController, sourceNode: PinchSourceContainerNode, getContentAreaInScreenSpace: @escaping () -> CGRect) {
self.controller = controller
self.sourceNode = sourceNode
self.getContentAreaInScreenSpace = getContentAreaInScreenSpace
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.dimNode.alpha = 0.0
self.clippingNode = ASDisplayNode()
self.clippingNode.clipsToBounds = true
self.scrollingContainer = ASDisplayNode()
super.init()
self.addSubnode(self.dimNode)
self.addSubnode(self.clippingNode)
self.clippingNode.addSubnode(self.scrollingContainer)
self.sourceNode.deactivate = { [weak self] in
guard let strongSelf = self else {
@ -254,12 +278,22 @@ private final class PinchControllerNode: ViewControllerTracingNode {
strongSelf.controller?.dismiss()
}
self.sourceNode.updated = { [weak self] scale, offset in
guard let strongSelf = self else {
self.sourceNode.updated = { [weak self] scale, pinchLocation, offset in
guard let strongSelf = self, let initialSourceFrame = strongSelf.initialSourceFrame else {
return
}
strongSelf.dimNode.alpha = max(0.0, min(1.0, scale - 1.0))
strongSelf.sourceNode.contentNode.transform = CATransform3DTranslate(CATransform3DMakeScale(scale, scale, 1.0), offset.x / scale, offset.y / scale, 0.0)
let pinchOffset = CGPoint(
x: pinchLocation.x - initialSourceFrame.width / 2.0,
y: pinchLocation.y - initialSourceFrame.height / 2.0
)
var transform = CATransform3DIdentity
transform = CATransform3DScale(transform, scale, scale, 0.0)
strongSelf.sourceNode.contentNode.transform = transform
strongSelf.sourceNode.contentNode.position = CGPoint(x: initialSourceFrame.midX + offset.x - pinchOffset.x * (scale - 1.0), y: initialSourceFrame.midY + offset.y - pinchOffset.y * (scale - 1.0))
}
}
@ -278,54 +312,95 @@ private final class PinchControllerNode: ViewControllerTracingNode {
self.validLayout = layout
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size))
}
func animateIn() {
let convertedFrame = convertFrame(self.sourceNode.contentNode.frame, from: self.sourceNode.view, to: self.view)
let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view)
self.sourceNode.contentNode.frame = convertedFrame
self.addSubnode(self.sourceNode.contentNode)
self.initialSourceFrame = convertedFrame
self.scrollingContainer.addSubnode(self.sourceNode.contentNode)
var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace()
updatedContentAreaInScreenSpace.origin.x = 0.0
updatedContentAreaInScreenSpace.size.width = self.bounds.width
self.clippingNode.layer.animateFrame(from: updatedContentAreaInScreenSpace, to: self.clippingNode.frame, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: updatedContentAreaInScreenSpace.minY, to: 0.0, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
func animateOut(completion: @escaping () -> Void) {
self.isAnimatingOut = true
let performCompletion: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.isAnimatingOut = false
strongSelf.sourceNode.restoreToNaturalSize()
strongSelf.sourceNode.addSubnode(strongSelf.sourceNode.contentNode)
completion()
}
if let (scale, offset) = self.sourceNode.gesture.currentTransform {
let duration = 0.4
let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view)
self.sourceNode.contentNode.frame = convertedFrame
self.initialSourceFrame = convertedFrame
if let (scale, pinchLocation, offset) = self.sourceNode.gesture.currentTransform, let initialSourceFrame = self.initialSourceFrame {
let duration = 0.3
let transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut
var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace()
updatedContentAreaInScreenSpace.origin.x = 0.0
updatedContentAreaInScreenSpace.size.width = self.bounds.width
self.clippingNode.layer.animateFrame(from: self.clippingNode.frame, to: updatedContentAreaInScreenSpace, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false)
let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: .spring)
if self.hapticFeedback == nil {
self.hapticFeedback = HapticFeedback()
}
self.hapticFeedback?.prepareImpact(.light)
Queue.mainQueue().after(0.2, { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.hapticFeedback?.impact(.light)
})
self.hapticFeedback?.impact(.light)
self.sourceNode.scaleUpdated?(1.0, transition)
let pinchOffset = CGPoint(
x: pinchLocation.x - initialSourceFrame.width / 2.0,
y: pinchLocation.y - initialSourceFrame.height / 2.0
)
var transform = CATransform3DIdentity
transform = CATransform3DScale(transform, scale, scale, 0.0)
self.sourceNode.contentNode.transform = CATransform3DIdentity
self.sourceNode.contentNode.position = CGPoint(x: initialSourceFrame.midX, y: initialSourceFrame.midY)
self.sourceNode.contentNode.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration * 1.2, damping: 110.0)
self.sourceNode.contentNode.layer.animatePosition(from: CGPoint(x: offset.x, y: offset.y), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { _ in
self.sourceNode.contentNode.layer.animatePosition(from: CGPoint(x: offset.x - pinchOffset.x * (scale - 1.0), y: offset.y - pinchOffset.y * (scale - 1.0)), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { _ in
performCompletion()
})
let dimNodeTransition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
let dimNodeTransition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: transitionCurve)
dimNodeTransition.updateAlpha(node: self.dimNode, alpha: 0.0)
} else {
performCompletion()
}
}
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
if self.isAnimatingOut {
self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset.y)
transition.animateOffsetAdditive(node: self.scrollingContainer, offset: -offset.y)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return nil
}
}
public final class PinchController: ViewController, StandalonePresentableController {
@ -335,6 +410,7 @@ public final class PinchController: ViewController, StandalonePresentableControl
}
private let sourceNode: PinchSourceContainerNode
private let getContentAreaInScreenSpace: () -> CGRect
private var wasDismissed = false
@ -342,8 +418,9 @@ public final class PinchController: ViewController, StandalonePresentableControl
return self.displayNode as! PinchControllerNode
}
public init(sourceNode: PinchSourceContainerNode) {
public init(sourceNode: PinchSourceContainerNode, getContentAreaInScreenSpace: @escaping () -> CGRect) {
self.sourceNode = sourceNode
self.getContentAreaInScreenSpace = getContentAreaInScreenSpace
super.init(navigationBarPresentationData: nil)
@ -361,7 +438,7 @@ public final class PinchController: ViewController, StandalonePresentableControl
}
override public func loadDisplayNode() {
self.displayNode = PinchControllerNode(controller: self, sourceNode: self.sourceNode)
self.displayNode = PinchControllerNode(controller: self, sourceNode: self.sourceNode, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace)
self.displayNodeDidLoad()
@ -392,4 +469,8 @@ public final class PinchController: ViewController, StandalonePresentableControl
})
}
}
public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
self.controllerNode.addRelativeContentOffset(offset, transition: transition)
}
}

View File

@ -62,7 +62,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case skipReadHistory(PresentationTheme, Bool)
case crashOnSlowQueries(PresentationTheme, Bool)
case clearTips(PresentationTheme)
case reimport(PresentationTheme)
case crash(PresentationTheme)
case resetData(PresentationTheme)
case resetDatabase(PresentationTheme)
case resetDatabaseAndCache(PresentationTheme)
@ -74,6 +74,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case knockoutWallpaper(PresentationTheme, Bool)
case demoVideoChats(Bool)
case experimentalCompatibility(Bool)
case enableNoiseSuppression(Bool)
case playerEmbedding(Bool)
case playlistPlayback(Bool)
case voiceConference
@ -93,7 +94,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.logging.rawValue
case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
return DebugControllerSection.experiments.rawValue
case .clearTips, .reimport, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .demoVideoChats, .experimentalCompatibility, .playerEmbedding, .playlistPlayback, .voiceConference:
case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .demoVideoChats, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableNoiseSuppression:
return DebugControllerSection.experiments.rawValue
case .preferredVideoCodec:
return DebugControllerSection.videoExperiments.rawValue
@ -134,7 +135,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 12
case .clearTips:
return 13
case .reimport:
case .crash:
return 14
case .resetData:
return 15
@ -158,14 +159,16 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 24
case .experimentalCompatibility:
return 25
case .playerEmbedding:
case .enableNoiseSuppression:
return 26
case .playlistPlayback:
case .playerEmbedding:
return 27
case .voiceConference:
case .playlistPlayback:
return 28
case .voiceConference:
return 29
case let .preferredVideoCodec(index, _, _, _):
return 29 + index
return 30 + index
case .disableVideoAspectScaling:
return 100
case .enableVoipTcp:
@ -553,20 +556,9 @@ private enum DebugControllerEntry: ItemListNodeEntry {
}).start()
}
})
case let .reimport(theme):
return ItemListActionItem(presentationData: presentationData, title: "Reimport Application Data", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
let appGroupName = "group.\(Bundle.main.bundleIdentifier!)"
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
guard let appGroupUrl = maybeAppGroupUrl else {
return
}
let statusPath = appGroupUrl.path + "/Documents/importcompleted"
if FileManager.default.fileExists(atPath: statusPath) {
let _ = try? FileManager.default.removeItem(at: URL(fileURLWithPath: statusPath))
exit(0)
}
case let .crash(theme):
return ItemListActionItem(presentationData: presentationData, title: "Crash", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
preconditionFailure()
})
case let .resetData(theme):
return ItemListActionItem(presentationData: presentationData, title: "Reset Data", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
@ -725,6 +717,16 @@ private enum DebugControllerEntry: ItemListNodeEntry {
})
}).start()
})
case let .enableNoiseSuppression(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Noise Suppression", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
var settings = settings as? ExperimentalUISettings ?? ExperimentalUISettings.defaultSettings
settings.enableNoiseSuppression = value
return settings
})
}).start()
})
case let .playerEmbedding(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
@ -822,6 +824,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
if isMainApp {
entries.append(.clearTips(presentationData.theme))
}
entries.append(.crash(presentationData.theme))
entries.append(.resetData(presentationData.theme))
entries.append(.resetDatabase(presentationData.theme))
entries.append(.resetDatabaseAndCache(presentationData.theme))
@ -834,6 +837,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
entries.append(.knockoutWallpaper(presentationData.theme, experimentalSettings.knockoutWallpaper))
entries.append(.demoVideoChats(experimentalSettings.demoVideoChats))
entries.append(.experimentalCompatibility(experimentalSettings.experimentalCompatibility))
entries.append(.enableNoiseSuppression(experimentalSettings.enableNoiseSuppression))
entries.append(.playerEmbedding(experimentalSettings.playerEmbedding))
entries.append(.playlistPlayback(experimentalSettings.playlistPlayback))
}

View File

@ -563,7 +563,13 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
guard let strongSelf = self, let controller = strongSelf.controller else {
return
}
let pinchController = PinchController(sourceNode: sourceNode)
let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: {
guard let strongSelf = self, let controller = strongSelf.controller else {
return CGRect()
}
return controller.view.convert(controller.view.bounds, to: nil)
})
controller.window?.presentInGlobalOverlay(pinchController)
}, openPeer: { [weak self] peerId in
self?.openPeer(peerId)

View File

@ -65,7 +65,7 @@ struct SqlitePreparedStatement {
}
return res == SQLITE_ROW
}
struct SqlError: Error {
var code: Int32
}
@ -234,10 +234,8 @@ public final class SqliteValueBox: ValueBox {
let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil)
let path = basePath + "/db_sqlite"
#if DEBUG
print("Instance \(self) opening sqlite at \(path)")
#endif
postboxLog("Instance \(self) opening sqlite at \(path)")
#if DEBUG
let exists = FileManager.default.fileExists(atPath: path)
@ -297,8 +295,10 @@ public final class SqliteValueBox: ValueBox {
let _ = try? FileManager.default.removeItem(atPath: path)
preconditionFailure("Couldn't open database")
}
sqlite3_busy_timeout(database.handle, 1000 * 10000)
postboxLog("Did open DB at \(path)")
sqlite3_busy_timeout(database.handle, 5 * 1000)
var resultCode: Bool = true
@ -306,8 +306,12 @@ public final class SqliteValueBox: ValueBox {
assert(resultCode)
resultCode = database.execute("PRAGMA cipher_default_plaintext_header_size=32")
assert(resultCode)
postboxLog("Did set up cipher")
if self.isEncrypted(database) {
postboxLog("Database is encrypted")
if let encryptionParameters = encryptionParameters {
precondition(encryptionParameters.salt.data.count == 16)
precondition(encryptionParameters.key.data.count == 32)
@ -316,12 +320,15 @@ public final class SqliteValueBox: ValueBox {
resultCode = database.execute("PRAGMA key=\"x'\(hexKey)'\"")
assert(resultCode)
postboxLog("Setting encryption key")
if self.isEncrypted(database) {
postboxLog("Encryption key is invalid")
if isTemporary || isReadOnly {
return nil
}
postboxLog("Encryption key is invalid")
for fileName in dabaseFileNames {
let _ = try? FileManager.default.removeItem(atPath: basePath + "/\(fileName)")
@ -354,6 +361,8 @@ public final class SqliteValueBox: ValueBox {
assert(resultCode)
}
} else if let encryptionParameters = encryptionParameters, encryptionParameters.forceEncryptionIfNoSet {
postboxLog("Not encrypted")
let hexKey = hexString(encryptionParameters.key.data + encryptionParameters.salt.data)
if FileManager.default.fileExists(atPath: path) {
@ -409,6 +418,8 @@ public final class SqliteValueBox: ValueBox {
}
}
}
postboxLog("Did set up encryption")
//database.execute("PRAGMA cache_size=-2097152")
resultCode = database.execute("PRAGMA mmap_size=0")
@ -421,6 +432,9 @@ public final class SqliteValueBox: ValueBox {
assert(resultCode)
resultCode = database.execute("PRAGMA cipher_memory_security = OFF")
assert(resultCode)
postboxLog("Did set up pragmas")
//resultCode = database.execute("PRAGMA wal_autocheckpoint=500")
//database.execute("PRAGMA journal_size_limit=1536")
@ -441,8 +455,12 @@ public final class SqliteValueBox: ValueBox {
let _ = self.runPragma(database, "checkpoint_fullfsync = 1")
assert(self.runPragma(database, "checkpoint_fullfsync") == "1")
postboxLog("Did set up checkpoint_fullfsync")
self.beginInternal(database: database)
postboxLog("Did begin transaction")
let result = self.getUserVersion(database)
@ -462,8 +480,12 @@ public final class SqliteValueBox: ValueBox {
for table in self.listFullTextTables(database) {
self.fullTextTables[table.id] = table
}
postboxLog("Did load tables")
self.commitInternal(database: database)
postboxLog("Did commit final")
lock.unlock()
@ -518,7 +540,21 @@ public final class SqliteValueBox: ValueBox {
private func isEncrypted(_ database: Database) -> Bool {
var statement: OpaquePointer? = nil
postboxLog("isEncrypted prepare...")
let allIsOk = Atomic<Bool>(value: false)
let databasePath = self.databasePath
DispatchQueue.global().asyncAfter(deadline: .now() + 5.0, execute: {
if allIsOk.with({ $0 }) == false {
postboxLog("Timeout reached, discarding database")
try? FileManager.default.removeItem(atPath: databasePath)
exit(0)
}
})
let status = sqlite3_prepare_v2(database.handle, "SELECT * FROM sqlite_master LIMIT 1", -1, &statement, nil)
let _ = allIsOk.swap(true)
postboxLog("isEncrypted prepare done")
if statement == nil {
postboxLog("isEncrypted: sqlite3_prepare_v2 status = \(status) [\(self.databasePath)]")
return true
@ -536,6 +572,7 @@ public final class SqliteValueBox: ValueBox {
preparedStatement.destroy()
return true
}
postboxLog("isEncrypted step done")
preparedStatement.destroy()
return status == SQLITE_NOTADB
}

View File

@ -347,7 +347,7 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
var title: String = ""
var speakerSubtitle: String = ""
let textFont = Font.regular(13.0)
let textFont = Font.with(size: 13.0, design: .regular, weight: .regular, traits: [.monospacedNumbers])
let textColor = UIColor.white
var segments: [AnimatedCountLabelNode.Segment] = []
var displaySpeakerSubtitle = false
@ -381,7 +381,22 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
}
displaySpeakerSubtitle = speakerSubtitle != title && !speakerSubtitle.isEmpty
if let membersCount = membersCount {
var requiresTimer = false
if let scheduleTime = self.currentGroupCallState?.info?.scheduleTimestamp {
requiresTimer = true
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let elapsedTime = scheduleTime - currentTime
let timerText: String
if elapsedTime >= 86400 {
timerText = timeIntervalString(strings: presentationData.strings, value: elapsedTime)
} else if elapsedTime < 0 {
timerText = presentationData.strings.VoiceChat_StatusLateBy(textForTimeout(value: abs(elapsedTime))).0
} else {
timerText = presentationData.strings.VoiceChat_StatusStartsIn(textForTimeout(value: elapsedTime)).0
}
segments.append(.text(0, NSAttributedString(string: timerText, font: textFont, textColor: textColor)))
} else if let membersCount = membersCount {
var membersPart = presentationData.strings.VoiceChat_Status_Members(membersCount)
if membersPart.contains("[") && membersPart.contains("]") {
if let startIndex = membersPart.firstIndex(of: "["), let endIndex = membersPart.firstIndex(of: "]") {
@ -433,6 +448,19 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
}
self.backgroundNode.connectingColor = color
if requiresTimer {
if self.currentCallTimer == nil {
let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
self?.update()
}, queue: Queue.mainQueue())
timer.start()
self.currentCallTimer = timer
}
} else if let currentCallTimer = self.currentCallTimer {
self.currentCallTimer = nil
currentCallTimer.invalidate()
}
}
if self.subtitleNode.segments != segments && !displaySpeakerSubtitle {

View File

@ -525,7 +525,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
if elapsedTime >= 86400 {
joinText = timeIntervalString(strings: strings, value: elapsedTime)
} else if elapsedTime < 0 {
joinText = "+\(textForTimeout(value: abs(elapsedTime)))"
joinText = "-\(textForTimeout(value: abs(elapsedTime)))"
} else {
joinText = textForTimeout(value: elapsedTime)
}

View File

@ -557,6 +557,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private var peerUpdatesSubscription: Disposable?
public private(set) var schedulePending = false
private var isScheduled = false
private var isScheduledStarted = false
init(
accountContext: AccountContext,
@ -581,6 +583,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.invite = invite
self.joinAsPeerId = joinAsPeerId ?? accountContext.account.peerId
self.schedulePending = initialCall == nil
self.isScheduled = initialCall == nil || initialCall?.scheduleTimestamp != nil
self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title, scheduleTimestamp: initialCall?.scheduleTimestamp, subscribedToScheduled: initialCall?.subscribedToScheduled ?? false)
self.statePromise = ValuePromise(self.stateValue)
@ -1058,6 +1061,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
case let .active(previousCallInfo):
if case let .active(callInfo) = internalState {
shouldJoin = previousCallInfo.scheduleTimestamp != nil && callInfo.scheduleTimestamp == nil
self.participantsContext = nil
activeCallInfo = callInfo
} else {
activeCallInfo = nil
@ -1082,6 +1086,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
outgoingAudioBitrateKbit = value
}
let enableNoiseSuppression = accountContext.sharedContext.immediateExperimentalUISettings.enableNoiseSuppression
callContext = OngoingGroupCallContext(video: self.videoCapturer, participantDescriptionsRequired: { [weak self] ssrcs in
Queue.mainQueue().async {
guard let strongSelf = self else {
@ -1098,7 +1104,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
strongSelf.requestCall(movingFromBroadcastToRtc: false)
}
}
}, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: self.isVideo)
}, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: self.isVideo, enableNoiseSuppression: enableNoiseSuppression)
self.incomingVideoSourcePromise.set(callContext.videoSources
|> deliverOnMainQueue
|> map { [weak self] sources -> [PeerId: UInt32] in
@ -1284,9 +1290,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
if !strongSelf.didConnectOnce {
strongSelf.didConnectOnce = true
let toneRenderer = PresentationCallToneRenderer(tone: .groupJoined)
strongSelf.toneRenderer = toneRenderer
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
if !strongSelf.isScheduled {
let toneRenderer = PresentationCallToneRenderer(tone: .groupJoined)
strongSelf.toneRenderer = toneRenderer
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
}
}
if let peer = strongSelf.reconnectingAsPeer {
@ -1711,6 +1719,180 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
if let isCurrentlyConnecting = self.isCurrentlyConnecting, isCurrentlyConnecting {
self.startCheckingCallIfNeeded()
}
} else if case let .active(callInfo) = internalState, callInfo.scheduleTimestamp != nil {
let accountContext = self.accountContext
let peerId = self.peerId
let rawAdminIds: Signal<Set<PeerId>, NoError>
if peerId.namespace == Namespaces.Peer.CloudChannel {
rawAdminIds = Signal { subscriber in
let (disposable, _) = accountContext.peerChannelMemberCategoriesContextsManager.admins(postbox: accountContext.account.postbox, network: accountContext.account.network, accountPeerId: accountContext.account.peerId, peerId: peerId, updated: { list in
var peerIds = Set<PeerId>()
for item in list.list {
if let adminInfo = item.participant.adminInfo, adminInfo.rights.rights.contains(.canManageCalls) {
peerIds.insert(item.peer.id)
}
}
subscriber.putNext(peerIds)
})
return disposable
}
|> distinctUntilChanged
|> runOn(.mainQueue())
} else {
rawAdminIds = accountContext.account.postbox.combinedView(keys: [.cachedPeerData(peerId: peerId)])
|> map { views -> Set<PeerId> in
guard let view = views.views[.cachedPeerData(peerId: peerId)] as? CachedPeerDataView else {
return Set()
}
guard let cachedData = view.cachedPeerData as? CachedGroupData, let participants = cachedData.participants else {
return Set()
}
return Set(participants.participants.compactMap { item -> PeerId? in
switch item {
case .creator, .admin:
return item.peerId
default:
return nil
}
})
}
|> distinctUntilChanged
}
let adminIds = combineLatest(queue: .mainQueue(),
rawAdminIds,
accountContext.account.postbox.combinedView(keys: [.basicPeer(peerId)])
)
|> map { rawAdminIds, view -> Set<PeerId> in
var rawAdminIds = rawAdminIds
if let peerView = view.views[.basicPeer(peerId)] as? BasicPeerView, let peer = peerView.peer as? TelegramChannel {
if peer.hasPermission(.manageCalls) {
rawAdminIds.insert(accountContext.account.peerId)
} else {
rawAdminIds.remove(accountContext.account.peerId)
}
}
return rawAdminIds
}
|> distinctUntilChanged
let participantsContext = GroupCallParticipantsContext(
account: self.accountContext.account,
peerId: self.peerId,
myPeerId: self.joinAsPeerId,
id: callInfo.id,
accessHash: callInfo.accessHash,
state: GroupCallParticipantsContext.State(
participants: [],
nextParticipantsFetchOffset: nil,
adminIds: Set(),
isCreator: false,
defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: self.stateValue.defaultParticipantMuteState == .muted, canChange: false),
sortAscending: true,
recordingStartTimestamp: nil,
title: self.stateValue.title,
scheduleTimestamp: self.stateValue.scheduleTimestamp,
subscribedToScheduled: self.stateValue.subscribedToScheduled,
totalCount: 0,
version: 0
),
previousServiceState: nil
)
self.temporaryParticipantsContext = nil
self.participantsContext = participantsContext
let myPeerId = self.joinAsPeerId
let myPeer = self.accountContext.account.postbox.transaction { transaction -> (Peer, CachedPeerData?)? in
if let peer = transaction.getPeer(myPeerId) {
return (peer, transaction.getPeerCachedData(peerId: myPeerId))
} else {
return nil
}
}
self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(),
participantsContext.state,
adminIds,
myPeer,
accountContext.account.postbox.peerView(id: peerId)
).start(next: { [weak self] state, adminIds, myPeerAndCachedData, view in
guard let strongSelf = self else {
return
}
var members = PresentationGroupCallMembers(
participants: [],
speakingParticipants: Set(),
totalCount: state.totalCount,
loadMoreToken: state.nextParticipantsFetchOffset
)
var participants: [GroupCallParticipantsContext.Participant] = []
var topParticipants: [GroupCallParticipantsContext.Participant] = []
if let (myPeer, cachedData) = myPeerAndCachedData {
let about: String?
if let cachedData = cachedData as? CachedUserData {
about = cachedData.about
} else if let cachedData = cachedData as? CachedUserData {
about = cachedData.about
} else {
about = nil
}
participants.append(GroupCallParticipantsContext.Participant(
peer: myPeer,
ssrc: nil,
jsonParams: nil,
joinTimestamp: strongSelf.temporaryJoinTimestamp,
raiseHandRating: strongSelf.temporaryRaiseHandRating,
hasRaiseHand: strongSelf.temporaryHasRaiseHand,
activityTimestamp: strongSelf.temporaryActivityTimestamp,
activityRank: strongSelf.temporaryActivityRank,
muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
volume: nil,
about: about
))
}
for participant in participants {
members.participants.append(participant)
if topParticipants.count < 3 {
topParticipants.append(participant)
}
}
strongSelf.membersValue = members
strongSelf.stateValue.adminIds = adminIds
strongSelf.stateValue.canManageCall = state.isCreator || adminIds.contains(strongSelf.accountContext.account.peerId)
if (state.isCreator || strongSelf.stateValue.adminIds.contains(strongSelf.accountContext.account.peerId)) && state.defaultParticipantsAreMuted.canChange {
strongSelf.stateValue.defaultParticipantMuteState = state.defaultParticipantsAreMuted.isMuted ? .muted : .unmuted
}
strongSelf.stateValue.recordingStartTimestamp = state.recordingStartTimestamp
strongSelf.stateValue.title = state.title
strongSelf.stateValue.scheduleTimestamp = strongSelf.isScheduledStarted ? nil : state.scheduleTimestamp
if state.scheduleTimestamp == nil && !strongSelf.isScheduledStarted {
strongSelf.updateSessionState(internalState: .active(GroupCallInfo(id: callInfo.id, accessHash: callInfo.accessHash, participantCount: state.totalCount, clientParams: callInfo.clientParams, streamDcId: callInfo.streamDcId, title: state.title, scheduleTimestamp: nil, subscribedToScheduled: false, recordingStartTimestamp: nil, sortAscending: true)), audioSessionControl: strongSelf.audioSessionControl)
} else {
strongSelf.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo(
id: callInfo.id,
accessHash: callInfo.accessHash,
participantCount: state.totalCount,
clientParams: nil,
streamDcId: nil,
title: state.title,
scheduleTimestamp: state.scheduleTimestamp,
subscribedToScheduled: false,
recordingStartTimestamp: state.recordingStartTimestamp,
sortAscending: state.sortAscending
))))
strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState(
participantCount: state.totalCount,
topParticipants: topParticipants,
activeSpeakers: Set()
)))
}
}))
}
}
}
@ -1886,11 +2068,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
return transaction.getPeer(peerId)
}
|> deliverOnMainQueue).start(next: { [weak self] myPeer in
guard let strongSelf = self, let _ = myPeer else {
guard let strongSelf = self, let myPeer = myPeer else {
return
}
strongSelf.reconnectingAsPeer = myPeer
let previousPeerId = strongSelf.joinAsPeerId
if let localSsrc = strongSelf.currentLocalSsrc {
@ -1898,23 +2078,30 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
strongSelf.joinAsPeerId = peerId
if let participantsContext = strongSelf.participantsContext, let immediateState = participantsContext.immediateState {
for participant in immediateState.participants {
if participant.peer.id == previousPeerId {
strongSelf.temporaryJoinTimestamp = participant.joinTimestamp
strongSelf.temporaryActivityTimestamp = participant.activityTimestamp
strongSelf.temporaryActivityRank = participant.activityRank
strongSelf.temporaryRaiseHandRating = participant.raiseHandRating
strongSelf.temporaryHasRaiseHand = participant.hasRaiseHand
strongSelf.temporaryMuteState = participant.muteState
}
}
strongSelf.switchToTemporaryParticipantsContext(sourceContext: participantsContext, oldMyPeerId: previousPeerId)
} else {
if strongSelf.stateValue.scheduleTimestamp != nil {
strongSelf.stateValue.myPeerId = peerId
strongSelf.reconnectedAsEventsPipe.putNext(myPeer)
} else {
strongSelf.reconnectingAsPeer = myPeer
if let participantsContext = strongSelf.participantsContext, let immediateState = participantsContext.immediateState {
for participant in immediateState.participants {
if participant.peer.id == previousPeerId {
strongSelf.temporaryJoinTimestamp = participant.joinTimestamp
strongSelf.temporaryActivityTimestamp = participant.activityTimestamp
strongSelf.temporaryActivityRank = participant.activityRank
strongSelf.temporaryRaiseHandRating = participant.raiseHandRating
strongSelf.temporaryHasRaiseHand = participant.hasRaiseHand
strongSelf.temporaryMuteState = participant.muteState
}
}
strongSelf.switchToTemporaryParticipantsContext(sourceContext: participantsContext, oldMyPeerId: previousPeerId)
} else {
strongSelf.stateValue.myPeerId = peerId
}
strongSelf.requestCall(movingFromBroadcastToRtc: false)
}
strongSelf.requestCall(movingFromBroadcastToRtc: false)
})
}
@ -2011,20 +2198,32 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.schedulePending = false
self.stateValue.scheduleTimestamp = timestamp
self.summaryParticipantsState.set(.single(SummaryParticipantsState(
participantCount: 1,
topParticipants: [],
activeSpeakers: Set()
)))
self.startDisposable.set((createGroupCall(account: self.account, peerId: self.peerId, title: nil, scheduleDate: timestamp)
|> deliverOnMainQueue).start(next: { [weak self] callInfo in
guard let strongSelf = self else {
return
}
strongSelf.updateSessionState(internalState: .active(callInfo), audioSessionControl: strongSelf.audioSessionControl)
}, error: { [weak self] error in
if let strongSelf = self {
strongSelf.markAsCanBeRemoved()
}
}))
}
public func startScheduled() {
guard case let .active(callInfo) = self.internalState else {
return
}
self.isScheduledStarted = true
self.stateValue.scheduleTimestamp = nil
self.startDisposable.set((startScheduledGroupCall(account: self.account, peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash)
@ -2033,6 +2232,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
return
}
strongSelf.updateSessionState(internalState: .active(callInfo), audioSessionControl: strongSelf.audioSessionControl)
let toneRenderer = PresentationCallToneRenderer(tone: .groupJoined)
strongSelf.toneRenderer = toneRenderer
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
}))
}
@ -2244,7 +2447,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
let account = self.account
let currentCall: Signal<GroupCallInfo?, CallError>
if let initialCall = self.initialCall {
currentCall = getCurrentGroupCall(account: account, callId: initialCall.id, accessHash: initialCall.accessHash)
@ -2254,6 +2456,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|> map { summary -> GroupCallInfo? in
return summary?.info
}
} else if case let .active(callInfo) = self.internalState {
currentCall = getCurrentGroupCall(account: account, callId: callInfo.id, accessHash: callInfo.accessHash)
|> mapError { _ -> CallError in
return .generic
}
|> map { summary -> GroupCallInfo? in
return summary?.info
}
} else {
currentCall = .single(nil)
}
@ -2319,7 +2529,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
guard let callInfo = self.internalState.callInfo else {
return
}
self.stateValue.title = title
self.stateValue.title = title.isEmpty ? nil : title
let _ = editGroupCallTitle(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, title: title).start()
}

View File

@ -242,7 +242,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
let subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
let totalHeight = titleSize.height + subtitleSize.height + 1.0
self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - totalHeight) / 2.0) + 88.0), size: titleSize)
self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - totalHeight) / 2.0) + 84.0), size: titleSize)
self.subtitleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: self.titleLabel.frame.maxY + 1.0), size: subtitleSize)
self.bottomNode.frame = CGRect(origin: CGPoint(), size: size)
@ -361,6 +361,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
}
var backgroundState: VoiceChatActionButtonBackgroundNode.State
var animated = true
switch state {
case let .button(text):
backgroundState = .button
@ -370,6 +371,9 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
self.buttonTitleLabel.frame = CGRect(origin: CGPoint(x: floor((self.bounds.width - titleSize.width) / 2.0), y: floor((self.bounds.height - titleSize.height) / 2.0)), size: titleSize)
case .scheduled:
backgroundState = .disabled
if previousState == .connecting {
animated = false
}
case let .active(state):
switch state {
case .on:
@ -385,7 +389,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
self.applyIconParams()
self.backgroundNode.isDark = dark
self.backgroundNode.update(state: backgroundState, animated: true)
self.backgroundNode.update(state: backgroundState, animated: animated)
if case .active = state, let previousState = previousState, case .connecting = previousState, animated {
self.activeDisposable.set((self.activePromise.get()
@ -755,7 +759,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
case muted
}
func updateGlowAndGradientAnimations(type: Gradient, previousType: Gradient? = nil) {
func updateGlowAndGradientAnimations(type: Gradient, previousType: Gradient? = nil, animated: Bool = true) {
let effectivePreviousTyoe = previousType ?? .active
let scale: CGFloat
@ -794,12 +798,14 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
self.maskGradientLayer.transform = CATransform3DMakeScale(targetScale, targetScale, 1.0)
if let _ = previousType {
self.maskGradientLayer.animateScale(from: initialScale, to: targetScale, duration: 0.3)
} else {
} else if animated {
self.maskGradientLayer.animateSpring(from: initialScale as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", duration: 0.45)
}
self.foregroundGradientLayer.colors = targetColors
self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
if animated {
self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
}
}
private func playMuteAnimation() {
@ -1081,6 +1087,16 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
self.playMuteAnimation()
}
self.transition = nil
} else {
if self.maskBlobView.isHidden {
self.updateGlowAndGradientAnimations(type: .muted, previousType: nil, animated: false)
self.maskCircleLayer.isHidden = false
self.maskProgressLayer.isHidden = true
self.maskGradientLayer.isHidden = false
self.maskBlobView.isHidden = false
self.maskBlobView.startAnimating()
self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45)
}
}
case .button:
self.updatedActive?(true)
@ -1537,7 +1553,6 @@ final class VoiceChatActionButtonIconNode: ManagedAnimationNode {
self.isColored = isColored
super.init(size: CGSize(width: 100.0, height: 100.0))
self.scale = 0.8
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.1))
}
@ -1632,15 +1647,25 @@ final class VoiceChatActionButtonIconNode: ManagedAnimationNode {
}
var useTiredAnimation = false
var useAngryAnimation = false
let val = Float.random(in: 0.0..<1.0)
if val <= 0.01 {
useTiredAnimation = true
} else if val <= 0.05 {
useAngryAnimation = true
}
let normalAnimations = ["VoiceHand_1", "VoiceHand_2", "VoiceHand_3", "VoiceHand_4", "VoiceHand_7"]
let normalAnimations = ["VoiceHand_1", "VoiceHand_2", "VoiceHand_3", "VoiceHand_4", "VoiceHand_7", "VoiceHand_8"]
let tiredAnimations = ["VoiceHand_5", "VoiceHand_6"]
let animations = useTiredAnimation ? tiredAnimations : normalAnimations
let angryAnimations = ["VoiceHand_9", "VoiceHand_10"]
let animations: [String]
if useTiredAnimation {
animations = tiredAnimations
} else if useAngryAnimation {
animations = angryAnimations
} else {
animations = normalAnimations
}
if let animationName = animations.randomElement() {
self.trackTo(item: ManagedAnimationItem(source: .local(animationName)))
}

View File

@ -830,6 +830,9 @@ public final class VoiceChatController: ViewController {
self.scheduleTextNode = ImmediateTextNode()
self.scheduleTextNode.isHidden = !self.isScheduling
self.scheduleTextNode.isUserInteractionEnabled = false
self.scheduleTextNode.textAlignment = .center
self.scheduleTextNode.maximumNumberOfLines = 4
self.scheduleCancelButton = SolidRoundedButtonNode(title: self.presentationData.strings.Common_Cancel, theme: SolidRoundedButtonTheme(backgroundColor: UIColor(rgb: 0x2b2b2f), foregroundColor: .white), height: 52.0, cornerRadius: 10.0)
self.scheduleCancelButton.isHidden = !self.isScheduling
@ -840,6 +843,7 @@ public final class VoiceChatController: ViewController {
self.dateFormatter.timeZone = TimeZone.current
self.timerNode = VoiceChatTimerNode(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat)
self.timerNode.isHidden = true
super.init()
@ -1258,10 +1262,17 @@ public final class VoiceChatController: ViewController {
}
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditBioTitle, text: presentationData.strings.VoiceChat_EditBioText, placeholder: presentationData.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: presentationData.strings.VoiceChat_EditBioSave, value: entry.about, maxLength: maxBioLength, apply: { bio in
if let strongSelf = self, let bio = bio {
let _ = (updateAbout(account: strongSelf.context.account, about: bio)
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}).start()
if peer.id.namespace == Namespaces.Peer.CloudUser {
let _ = (updateAbout(account: strongSelf.context.account, about: bio)
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}).start()
} else {
let _ = (updatePeerTitle(account: strongSelf.context.account, peerId: peer.id, title: bio)
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}).start()
}
strongSelf.presentUndoOverlay(content: .info(text: strongSelf.presentationData.strings.VoiceChat_EditBioSuccess), action: { _ in return false })
}
@ -1501,6 +1512,7 @@ public final class VoiceChatController: ViewController {
self.contentContainer.addSubnode(self.rightBorderNode)
self.contentContainer.addSubnode(self.bottomPanelNode)
self.contentContainer.addSubnode(self.timerNode)
self.contentContainer.addSubnode(self.scheduleTextNode)
let invitedPeers: Signal<[Peer], NoError> = self.call.invitedPeers
|> mapToSignal { ids -> Signal<[Peer], NoError> in
@ -1700,7 +1712,7 @@ public final class VoiceChatController: ViewController {
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in
if let strongSelf = self {
strongSelf.currentContentOffset = offset
if strongSelf.animation == nil && !strongSelf.animatingExpansion {
if !strongSelf.animatingExpansion && !strongSelf.animatingInsertion && strongSelf.panGestureArguments == nil {
strongSelf.updateFloatingHeaderOffset(offset: offset, transition: transition)
}
}
@ -1994,7 +2006,7 @@ public final class VoiceChatController: ViewController {
}
}
items.append(.action(ContextMenuActionItem(text: strongSelf.isNoiseSuppressionEnabled ? "Disable Noise Suppression" : "Enable Noise Suppression", textColor: .primary, icon: { theme in
/*items.append(.action(ContextMenuActionItem(text: strongSelf.isNoiseSuppressionEnabled ? "Disable Noise Suppression" : "Enable Noise Suppression", textColor: .primary, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
@ -2004,7 +2016,7 @@ public final class VoiceChatController: ViewController {
}
strongSelf.call.setIsNoiseSuppressionEnabled(!strongSelf.isNoiseSuppressionEnabled)
})))
})))*/
if let callState = strongSelf.callState, callState.canManageCall {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EndVoiceChat, textColor: .destructive, icon: { theme in
@ -2206,7 +2218,7 @@ public final class VoiceChatController: ViewController {
self.view.addGestureRecognizer(panRecognizer)
if self.isScheduling {
self.setupPickerView()
self.setupSchedulePickerView()
self.updateScheduleButtonTitle()
}
}
@ -2218,22 +2230,27 @@ public final class VoiceChatController: ViewController {
let currentDate = Date()
var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: currentDate)
components.second = 0
let minute = (components.minute ?? 0) % 5
let next1MinDate = calendar.date(byAdding: .minute, value: 1, to: calendar.date(from: components)!)
let next5MinDate = calendar.date(byAdding: .minute, value: 5 - minute, to: calendar.date(from: components)!)
let roundedDate = calendar.date(from: components)!
let next1MinDate = calendar.date(byAdding: .minute, value: 1, to: roundedDate)
components.minute = 0
let roundedToHourDate = calendar.date(from: components)!
let nextTwoHourDate = calendar.date(byAdding: .hour, value: 2, to: roundedToHourDate)
let maxDate = calendar.date(byAdding: .day, value: 7, to: currentDate)
if let date = calendar.date(byAdding: .day, value: 365, to: currentDate) {
self.pickerView?.maximumDate = date
}
if let next1MinDate = next1MinDate, let next5MinDate = next5MinDate {
if let next1MinDate = next1MinDate, let nextTwoHourDate = nextTwoHourDate {
self.pickerView?.minimumDate = next1MinDate
self.pickerView?.date = next5MinDate
self.pickerView?.maximumDate = maxDate
self.pickerView?.date = nextTwoHourDate
}
}
private func setupPickerView() {
private func setupSchedulePickerView() {
var currentDate: Date?
if let pickerView = self.pickerView {
currentDate = pickerView.date
@ -2271,7 +2288,9 @@ public final class VoiceChatController: ViewController {
}
let calendar = Calendar(identifier: .gregorian)
let time = stringForMessageTimestamp(timestamp: Int32(date.timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat)
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let timestamp = Int32(date.timeIntervalSince1970)
let time = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: self.presentationData.dateTimeFormat)
let buttonTitle: String
if calendar.isDateInToday(date) {
buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleToday(time).0
@ -2282,6 +2301,15 @@ public final class VoiceChatController: ViewController {
}
self.scheduleButtonTitle = buttonTitle
let delta = timestamp - currentTimestamp
var isGroup = true
if let peer = self.peer as? TelegramChannel, case .broadcast = peer.info {
isGroup = false
}
let intervalString = timeIntervalString(strings: self.presentationData.strings, value: max(60, delta))
self.scheduleTextNode.attributedText = NSAttributedString(string: isGroup ? self.presentationData.strings.ScheduleVoiceChat_GroupText(intervalString).0 : self.presentationData.strings.ScheduleVoiceChat_ChannelText(intervalString).0, font: Font.regular(14.0), textColor: UIColor(rgb: 0x8e8e93))
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
}
@ -2341,16 +2369,22 @@ public final class VoiceChatController: ViewController {
self.actionButton.titleLabel.layer.animatePosition(from: CGPoint(x: 0.0, y: -26.0), to: CGPoint(), duration: 0.2, additive: true)
if let pickerView = self.pickerView {
self.pickerView = nil
pickerView.alpha = 0.0
pickerView.layer.animateScale(from: 1.0, to: 0.25, duration: 0.15, removeOnCompletion: false)
pickerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
pickerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak pickerView] _ in
pickerView?.removeFromSuperview()
})
pickerView.isUserInteractionEnabled = false
}
self.timerNode.alpha = 1.0
self.timerNode.isHidden = false
self.timerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.timerNode.animateIn()
self.scheduleTextNode.alpha = 0.0
self.scheduleTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
self.updateTitle(slide: true, transition: .animated(duration: 0.2, curve: .easeInOut))
}
@ -2362,7 +2396,9 @@ public final class VoiceChatController: ViewController {
self.listNode.isUserInteractionEnabled = true
self.timerNode.alpha = 0.0
self.timerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
self.timerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
self?.timerNode.isHidden = true
})
self.updateTitle(transition: .animated(duration: 0.2, curve: .easeInOut))
}
@ -2602,7 +2638,6 @@ public final class VoiceChatController: ViewController {
self.schedule()
} else if callState.canManageCall {
self.call.startScheduled()
self.transitionToCall()
} else {
self.call.toggleScheduledSubscription(!callState.subscribedToScheduled)
}
@ -2678,8 +2713,13 @@ public final class VoiceChatController: ViewController {
let _ = (self.inviteLinksPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] inviteLinks in
guard let strongSelf = self else {
return
}
if let inviteLinks = inviteLinks {
self?.presentShare(inviteLinks)
strongSelf.presentShare(inviteLinks)
} else if let addressName = strongSelf.peer?.addressName, !addressName.isEmpty {
strongSelf.presentShare(GroupCallInviteLinks(listenerLink: "https://t.me/\(addressName)?voicechat", speakerLink: nil))
}
})
return
@ -2790,8 +2830,6 @@ public final class VoiceChatController: ViewController {
} else {
topInset = max(0.0, panInitialTopInset + min(0.0, panOffset))
}
} else if let _ = self.animation {
topInset = self.listNode.frame.minY - listTopInset
} else if let currentTopInset = self.topInset {
topInset = self.isExpanded ? 0.0 : currentTopInset
} else {
@ -2841,27 +2879,20 @@ public final class VoiceChatController: ViewController {
completion?()
}
self.topPanelBackgroundNode.frame = CGRect(x: 0.0, y: topPanelHeight - 24.0, width: size.width, height: 24.0)
var bottomEdge: CGFloat = 0.0
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListViewItemNode {
let convertedFrame = self.listNode.view.convert(itemNode.frame, to: self.view)
let convertedFrame = self.listNode.view.convert(itemNode.frame, to: self.contentContainer.view)
if convertedFrame.maxY > bottomEdge {
bottomEdge = convertedFrame.maxY
}
}
}
let listMaxY = listTopInset + listSize.height
if bottomEdge.isZero {
bottomEdge = listMaxY
}
var bottomOffset: CGFloat = 0.0
if bottomEdge < listMaxY && (self.panGestureArguments != nil || self.isExpanded) {
bottomOffset = bottomEdge - listMaxY
}
let bottomOffset: CGFloat = min(0.0, bottomEdge - listMaxY)
let bottomCornersFrame = CGRect(origin: CGPoint(x: sideInset, y: -50.0 + bottomOffset), size: CGSize(width: size.width - sideInset * 2.0, height: 50.0))
let previousBottomCornersFrame = self.bottomCornersNode.frame
if !bottomCornersFrame.equalTo(previousBottomCornersFrame) {
@ -3139,9 +3170,7 @@ public final class VoiceChatController: ViewController {
topInset = listSize.height
}
if self.animation == nil {
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset + topInset), size: listSize))
}
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset + topInset), size: listSize))
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listSize, insets: insets, duration: duration, curve: curve)
@ -3161,6 +3190,9 @@ public final class VoiceChatController: ViewController {
transition.updateFrame(node: self.timerNode, frame: timerFrame)
self.timerNode.update(size: timerFrame.size, scheduleTime: self.callState?.scheduleTimestamp, transition: .immediate)
let scheduleTextSize = self.scheduleTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
self.scheduleTextNode.frame = CGRect(origin: CGPoint(x: floor((size.width - scheduleTextSize.width) / 2.0), y: layout.size.height - layout.intrinsicInsets.bottom - scheduleTextSize.height - 145.0), size: scheduleTextSize)
let centralButtonSide = min(size.width, size.height) - 32.0
let centralButtonSize = CGSize(width: centralButtonSide, height: centralButtonSide)
let cameraButtonSize = CGSize(width: 36.0, height: 36.0)
@ -3188,7 +3220,7 @@ public final class VoiceChatController: ViewController {
smallButtons = false
firstButtonFrame = CGRect(origin: CGPoint(x: floor(leftButtonFrame.midX - cameraButtonSize.width / 2.0), y: leftButtonFrame.minY - upperButtonDistance - cameraButtonSize.height), size: cameraButtonSize)
secondButtonFrame = leftButtonFrame
thirdButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - centralButtonSize.width) / 2.0), y: floorToScreenPixels((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0)), size: centralButtonSize)
thirdButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - centralButtonSize.width) / 2.0), y: floor((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0) - 3.0), size: centralButtonSize)
forthButtonFrame = rightButtonFrame
case let .fullscreen(controlsHidden):
smallButtons = true
@ -3398,10 +3430,13 @@ public final class VoiceChatController: ViewController {
self.enqueuedTransitions.remove(at: 0)
if self.callState?.scheduleTimestamp != nil && self.listNode.alpha > 0.0 {
self.timerNode.isHidden = false
self.listNode.alpha = 0.0
self.listNode.isUserInteractionEnabled = false
self.backgroundNode.backgroundColor = panelBackgroundColor
self.updateIsFullscreen(false)
} else if self.callState?.scheduleTimestamp == nil && !self.isScheduling && self.listNode.alpha == 0.0 {
self.transitionToCall()
}
var options = ListViewDeleteAndInsertOptions()
@ -3412,7 +3447,7 @@ public final class VoiceChatController: ViewController {
if transition.crossFade {
options.insert(.AnimateCrossfade)
}
if transition.animated && self.animation == nil {
if transition.animated {
options.insert(.AnimateInsertion)
}
}
@ -3443,11 +3478,7 @@ public final class VoiceChatController: ViewController {
let listTopInset = layoutTopInset + 63.0
let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight)
if self.isScheduling || self.callState?.scheduleTimestamp != nil {
self.topInset = listSize.height - 46.0 - floor(56.0 * 3.5)
} else {
self.topInset = max(0.0, max(listSize.height - itemsHeight, listSize.height - 46.0 - floor(56.0 * 3.5)))
}
self.topInset = listSize.height - 46.0 - floor(56.0 * 3.5)
let targetY = listTopInset + (self.topInset ?? listSize.height)
@ -3455,24 +3486,20 @@ public final class VoiceChatController: ViewController {
var frame = self.listNode.frame
frame.origin.y = targetY
self.listNode.frame = frame
} else if !self.isExpanded {
if self.listNode.frame.minY != targetY && !self.animatingExpansion && self.panGestureArguments == nil {
self.animation = ListViewAnimation(from: self.listNode.frame.minY, to: targetY, duration: 0.4, curve: listViewAnimationCurveSystem, beginAt: CACurrentMediaTime(), update: { [weak self] _, currentValue in
if let strongSelf = self {
var frame = strongSelf.listNode.frame
frame.origin.y = currentValue
strongSelf.listNode.frame = frame
strongSelf.updateFloatingHeaderOffset(offset: strongSelf.currentContentOffset ?? 0.0, transition: .immediate)
}
})
self.updateAnimation()
}
}
if transition.animated {
self.animatingInsertion = true
}
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: nil, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
if strongSelf.animatingInsertion {
strongSelf.updateFloatingHeaderOffset(offset: self?.currentContentOffset ?? 0.0, transition: .animated(duration: 0.2, curve: .easeInOut))
strongSelf.animatingInsertion = false
}
if !strongSelf.didSetContentsReady {
strongSelf.didSetContentsReady = true
strongSelf.controller?.contentsReady.set(true)
@ -3480,39 +3507,6 @@ public final class VoiceChatController: ViewController {
})
}
private var animator: ConstantDisplayLinkAnimator?
private var animation: ListViewAnimation?
private func updateAnimation() {
var animate = false
let timestamp = CACurrentMediaTime()
if let animation = self.animation {
animation.applyAt(timestamp)
if animation.completeAt(timestamp) {
self.animation = nil
} else {
animate = true
}
}
if animate {
let animator: ConstantDisplayLinkAnimator
if let current = self.animator {
animator = current
} else {
animator = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.updateAnimation()
})
self.animator = animator
}
animator.isPaused = false
} else {
self.animator?.isPaused = true
}
}
private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, callMembers: ([GroupCallParticipantsContext.Participant], String?), invitedPeers: [Peer], speakingPeers: Set<PeerId>) {
var disableAnimation = false
if self.currentCallMembers?.1 != callMembers.1 {
@ -3689,6 +3683,8 @@ public final class VoiceChatController: ViewController {
self.itemInteraction?.isExpanded = self.isExpanded
}
}
private var animatingInsertion = false
private var animatingExpansion = false
private var panGestureArguments: (topInset: CGFloat, offset: CGFloat)?
@ -3914,8 +3910,9 @@ public final class VoiceChatController: ViewController {
return
}
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_EditTitleTitle, text: strongSelf.presentationData.strings.VoiceChat_EditTitleText, placeholder: chatPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: strongSelf.callState?.title, maxLength: 40, apply: { title in
if let strongSelf = self, let title = title {
let initialTitle = strongSelf.callState?.title ?? ""
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_EditTitleTitle, text: strongSelf.presentationData.strings.VoiceChat_EditTitleText, placeholder: chatPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { title in
if let strongSelf = self, let title = title, title != initialTitle {
strongSelf.call.updateTitle(title)
strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).0), action: { _ in return false })

View File

@ -158,12 +158,12 @@ final class VoiceChatMicrophoneNode: ASDisplayNode {
context.setFillColor(parameters.color.cgColor)
var clearLineWidth: CGFloat = 4.0
var clearLineWidth: CGFloat = 2.0
var lineWidth: CGFloat = 1.0 + UIScreenPixel
if bounds.size.width > 36.0 {
context.scaleBy(x: 2.0, y: 2.0)
} else if bounds.size.width < 30.0 {
clearLineWidth = 3.0
clearLineWidth = 2.0
lineWidth = 1.0
}
@ -207,18 +207,19 @@ final class VoiceChatMicrophoneNode: ASDisplayNode {
}
if parameters.reverse {
startPoint = CGPoint(x: origin.x + length * (1.0 - parameters.transition), y: origin.y + length * (1.0 - parameters.transition))
endPoint = CGPoint(x: origin.x + length, y: origin.y + length)
startPoint = CGPoint(x: origin.x + length * (1.0 - parameters.transition), y: origin.y + length * (1.0 - parameters.transition)).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel)
endPoint = CGPoint(x: origin.x + length, y: origin.y + length).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel)
} else {
startPoint = origin
endPoint = CGPoint(x: origin.x + length * parameters.transition, y: origin.y + length * parameters.transition)
startPoint = origin.offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel)
endPoint = CGPoint(x: origin.x + length * parameters.transition, y: origin.y + length * parameters.transition).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel)
}
context.setBlendMode(.clear)
context.setLineWidth(clearLineWidth)
context.move(to: startPoint)
context.addLine(to: endPoint)
context.move(to: startPoint.offsetBy(dx: 0.0, dy: 1.0 + UIScreenPixel))
context.addLine(to: endPoint.offsetBy(dx: 0.0, dy: 1.0 + UIScreenPixel))
context.strokePath()
context.setBlendMode(.normal)

View File

@ -169,7 +169,7 @@ public final class VoiceChatOverlayController: ViewController {
if reclaim {
self.dismissed = true
let targetPosition = CGPoint(x: layout.size.width / 2.0, y: layout.size.height - layout.intrinsicInsets.bottom - 205.0 / 2.0)
let targetPosition = CGPoint(x: layout.size.width / 2.0, y: layout.size.height - layout.intrinsicInsets.bottom - 205.0 / 2.0 - 2.0)
if self.isSlidOffscreen {
self.isSlidOffscreen = false
self.isButtonHidden = true

View File

@ -133,10 +133,8 @@ final class VoiceChatTimerNode: ASDisplayNode {
let timerText: String
if elapsedTime >= 86400 {
timerText = timeIntervalString(strings: self.strings, value: elapsedTime)
} else if elapsedTime < 0 {
timerText = "\(textForTimeout(value: abs(elapsedTime)))"
} else {
timerText = textForTimeout(value: elapsedTime)
timerText = textForTimeout(value: abs(elapsedTime))
}
if self.updateTimer == nil {

View File

@ -551,6 +551,10 @@ private final class VoiceChatUserNameEditAlertContentNode: AlertContentNode {
}
self.updateTheme(theme)
self.firstNameInputFieldNode.complete = { [weak self] in
self?.lastNameInputFieldNode.activateInput()
}
}
deinit {

View File

@ -393,11 +393,12 @@ public func sendBotPaymentForm(account: Account, messageId: MessageId, formId: I
}
}
public struct BotPaymentReceipt : Equatable {
public struct BotPaymentReceipt {
public let invoice: BotPaymentInvoice
public let info: BotPaymentRequestedInfo?
public let shippingOption: BotPaymentShippingOption?
public let credentialsTitle: String
public let invoiceMedia: TelegramMediaInvoice
public let tipAmount: Int64?
}
@ -419,14 +420,55 @@ public func requestBotPaymentReceipt(account: Account, messageId: MessageId) ->
|> mapError { _ -> RequestBotPaymentReceiptError in
return .generic
}
|> map { result -> BotPaymentReceipt in
switch result {
case let .paymentReceipt(flags, date, botId, providerId, title, description, photo, invoice, info, shipping, tipAmount, currency, totalAmount, credentialsTitle, users):
let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice)
let parsedInfo = info.flatMap(BotPaymentRequestedInfo.init)
let shippingOption = shipping.flatMap(BotPaymentShippingOption.init)
return BotPaymentReceipt(invoice: parsedInvoice, info: parsedInfo, shippingOption: shippingOption, credentialsTitle: credentialsTitle, tipAmount: tipAmount)
|> mapToSignal { result -> Signal<BotPaymentReceipt, RequestBotPaymentReceiptError> in
return account.postbox.transaction { transaction -> BotPaymentReceipt in
switch result {
case let .paymentReceipt(flags, date, botId, providerId, title, description, photo, invoice, info, shipping, tipAmount, currency, totalAmount, credentialsTitle, users):
var peers: [Peer] = []
for user in users {
peers.append(TelegramUser(user: user))
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated in return updated })
let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice)
let parsedInfo = info.flatMap(BotPaymentRequestedInfo.init)
let shippingOption = shipping.flatMap(BotPaymentShippingOption.init)
/*let fields = BotPaymentInvoiceFields()
let form = BotPaymentForm(
id: 0,
canSaveCredentials: false,
passwordMissing: false,
invoice: BotPaymentInvoice(
isTest: false,
requestedFields: fields,
currency: currency,
prices: [],
tip: nil
),
providerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(providerId)),
url: "",
nativeProvider: nil,
savedInfo: nil,
savedCredentials: nil
)*/
let invoiceMedia = TelegramMediaInvoice(
title: title,
description: description,
photo: photo.flatMap(TelegramMediaWebFile.init),
receiptMessageId: nil,
currency: currency,
totalAmount: totalAmount,
startParam: "",
flags: []
)
return BotPaymentReceipt(invoice: parsedInvoice, info: parsedInfo, shippingOption: shippingOption, credentialsTitle: credentialsTitle, invoiceMedia: invoiceMedia, tipAmount: tipAmount)
}
}
|> castError(RequestBotPaymentReceiptError.self)
}
}
}

View File

@ -184,6 +184,7 @@ public func getCurrentGroupCall(account: Account, callId: Int64, accessHash: Int
public enum CreateGroupCallError {
case generic
case anonymousNotAllowed
case scheduledTooLate
}
public func createGroupCall(account: Account, peerId: PeerId, title: String?, scheduleDate: Int32?) -> Signal<GroupCallInfo, CreateGroupCallError> {
@ -208,6 +209,8 @@ public func createGroupCall(account: Account, peerId: PeerId, title: String?, sc
|> mapError { error -> CreateGroupCallError in
if error.errorDescription == "ANONYMOUS_CALLS_DISABLED" {
return .anonymousNotAllowed
} else if error.errorDescription == "SCHEDULE_DATE_TOO_LATE" {
return .scheduledTooLate
}
return .generic
}
@ -1005,6 +1008,11 @@ public final class GroupCallParticipantsContext {
public struct DefaultParticipantsAreMuted: Equatable {
public var isMuted: Bool
public var canChange: Bool
public init(isMuted: Bool, canChange: Bool) {
self.isMuted = isMuted
self.canChange = canChange
}
}
public var participants: [Participant]
@ -1037,6 +1045,34 @@ public final class GroupCallParticipantsContext {
self.participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: self.sortAscending) })
}
public init(
participants: [Participant],
nextParticipantsFetchOffset: String?,
adminIds: Set<PeerId>,
isCreator: Bool,
defaultParticipantsAreMuted: DefaultParticipantsAreMuted,
sortAscending: Bool,
recordingStartTimestamp: Int32?,
title: String?,
scheduleTimestamp: Int32?,
subscribedToScheduled: Bool,
totalCount: Int,
version: Int32
) {
self.participants = participants
self.nextParticipantsFetchOffset = nextParticipantsFetchOffset
self.adminIds = adminIds
self.isCreator = isCreator
self.defaultParticipantsAreMuted = defaultParticipantsAreMuted
self.sortAscending = sortAscending
self.recordingStartTimestamp = recordingStartTimestamp
self.title = title
self.scheduleTimestamp = scheduleTimestamp
self.subscribedToScheduled = subscribedToScheduled
self.totalCount = totalCount
self.version = version
}
}
private struct OverlayState: Equatable {

View File

@ -359,7 +359,13 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
pageIndicatorInactiveColor: UIColor(white: 1.0, alpha: 0.3),
inputClearButtonColor: UIColor(rgb: 0x8b9197),
itemBarChart: PresentationThemeItemBarChart(color1: UIColor(rgb: 0xffffff), color2: UIColor(rgb: 0x929196), color3: UIColor(rgb: 0x333333)),
itemInputField: PresentationInputFieldTheme(backgroundColor: UIColor(rgb: 0x0f0f0f), strokeColor: UIColor(rgb: 0x0f0f0f), placeholderColor: UIColor(rgb: 0x8f8f8f), primaryColor: UIColor(rgb: 0xffffff), controlColor: UIColor(rgb: 0x8f8f8f))
itemInputField: PresentationInputFieldTheme(backgroundColor: UIColor(rgb: 0x0f0f0f), strokeColor: UIColor(rgb: 0x0f0f0f), placeholderColor: UIColor(rgb: 0x8f8f8f), primaryColor: UIColor(rgb: 0xffffff), controlColor: UIColor(rgb: 0x8f8f8f)),
paymentOption: PresentationThemeList.PaymentOption(
inactiveFillColor: UIColor(rgb: 0x00A650).withMultipliedAlpha(0.3),
inactiveForegroundColor: UIColor(rgb: 0x00A650),
activeFillColor: UIColor(rgb: 0x00A650),
activeForegroundColor: UIColor(rgb: 0xffffff)
)
)
let chatList = PresentationThemeChatList(

View File

@ -612,7 +612,13 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres
pageIndicatorInactiveColor: mainSecondaryTextColor.withAlphaComponent(0.4),
inputClearButtonColor: mainSecondaryColor,
itemBarChart: PresentationThemeItemBarChart(color1: accentColor, color2: mainSecondaryTextColor.withAlphaComponent(0.5), color3: accentColor.withMultiplied(hue: 1.038, saturation: 0.329, brightness: 0.33)),
itemInputField: PresentationInputFieldTheme(backgroundColor: mainInputColor, strokeColor: mainInputColor, placeholderColor: mainSecondaryColor, primaryColor: UIColor(rgb: 0xffffff), controlColor: mainSecondaryColor)
itemInputField: PresentationInputFieldTheme(backgroundColor: mainInputColor, strokeColor: mainInputColor, placeholderColor: mainSecondaryColor, primaryColor: UIColor(rgb: 0xffffff), controlColor: mainSecondaryColor),
paymentOption: PresentationThemeList.PaymentOption(
inactiveFillColor: UIColor(rgb: 0x00A650).withMultipliedAlpha(0.3),
inactiveForegroundColor: UIColor(rgb: 0x00A650),
activeFillColor: UIColor(rgb: 0x00A650),
activeForegroundColor: UIColor(rgb: 0xffffff)
)
)
let chatList = PresentationThemeChatList(

View File

@ -448,7 +448,13 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
pageIndicatorInactiveColor: UIColor(rgb: 0xe3e3e7),
inputClearButtonColor: UIColor(rgb: 0xcccccc),
itemBarChart: PresentationThemeItemBarChart(color1: UIColor(rgb: 0x007ee5), color2: UIColor(rgb: 0xc8c7cc), color3: UIColor(rgb: 0xf2f1f7)),
itemInputField: PresentationInputFieldTheme(backgroundColor: UIColor(rgb: 0xf2f2f7), strokeColor: UIColor(rgb: 0xf2f2f7), placeholderColor: UIColor(rgb: 0xb6b6bb), primaryColor: UIColor(rgb: 0x000000), controlColor: UIColor(rgb: 0xb6b6bb))
itemInputField: PresentationInputFieldTheme(backgroundColor: UIColor(rgb: 0xf2f2f7), strokeColor: UIColor(rgb: 0xf2f2f7), placeholderColor: UIColor(rgb: 0xb6b6bb), primaryColor: UIColor(rgb: 0x000000), controlColor: UIColor(rgb: 0xb6b6bb)),
paymentOption: PresentationThemeList.PaymentOption(
inactiveFillColor: UIColor(rgb: 0x00A650).withMultipliedAlpha(0.1),
inactiveForegroundColor: UIColor(rgb: 0x00A650),
activeFillColor: UIColor(rgb: 0x00A650),
activeForegroundColor: UIColor(rgb: 0xffffff)
)
)
let chatList = PresentationThemeChatList(

View File

@ -406,6 +406,25 @@ public final class PresentationInputFieldTheme {
}
public final class PresentationThemeList {
public final class PaymentOption {
public let inactiveFillColor: UIColor
public let inactiveForegroundColor: UIColor
public let activeFillColor: UIColor
public let activeForegroundColor: UIColor
public init(
inactiveFillColor: UIColor,
inactiveForegroundColor: UIColor,
activeFillColor: UIColor,
activeForegroundColor: UIColor
) {
self.inactiveFillColor = inactiveFillColor
self.inactiveForegroundColor = inactiveForegroundColor
self.activeFillColor = activeFillColor
self.activeForegroundColor = activeForegroundColor
}
}
public let blocksBackgroundColor: UIColor
public let plainBackgroundColor: UIColor
public let itemPrimaryTextColor: UIColor
@ -437,8 +456,42 @@ public final class PresentationThemeList {
public let inputClearButtonColor: UIColor
public let itemBarChart: PresentationThemeItemBarChart
public let itemInputField: PresentationInputFieldTheme
public let paymentOption: PaymentOption
public init(blocksBackgroundColor: UIColor, plainBackgroundColor: UIColor, itemPrimaryTextColor: UIColor, itemSecondaryTextColor: UIColor, itemDisabledTextColor: UIColor, itemAccentColor: UIColor, itemHighlightedColor: UIColor, itemDestructiveColor: UIColor, itemPlaceholderTextColor: UIColor, itemBlocksBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, itemBlocksSeparatorColor: UIColor, itemPlainSeparatorColor: UIColor, disclosureArrowColor: UIColor, sectionHeaderTextColor: UIColor, freeTextColor: UIColor, freeTextErrorColor: UIColor, freeTextSuccessColor: UIColor, freeMonoIconColor: UIColor, itemSwitchColors: PresentationThemeSwitch, itemDisclosureActions: PresentationThemeItemDisclosureActions, itemCheckColors: PresentationThemeFillStrokeForeground, controlSecondaryColor: UIColor, freeInputField: PresentationInputFieldTheme, freePlainInputField: PresentationInputFieldTheme, mediaPlaceholderColor: UIColor, scrollIndicatorColor: UIColor, pageIndicatorInactiveColor: UIColor, inputClearButtonColor: UIColor, itemBarChart: PresentationThemeItemBarChart, itemInputField: PresentationInputFieldTheme) {
public init(
blocksBackgroundColor: UIColor,
plainBackgroundColor: UIColor,
itemPrimaryTextColor: UIColor,
itemSecondaryTextColor: UIColor,
itemDisabledTextColor: UIColor,
itemAccentColor: UIColor,
itemHighlightedColor: UIColor,
itemDestructiveColor: UIColor,
itemPlaceholderTextColor: UIColor,
itemBlocksBackgroundColor: UIColor,
itemHighlightedBackgroundColor: UIColor,
itemBlocksSeparatorColor: UIColor,
itemPlainSeparatorColor: UIColor,
disclosureArrowColor: UIColor,
sectionHeaderTextColor: UIColor,
freeTextColor: UIColor,
freeTextErrorColor: UIColor,
freeTextSuccessColor: UIColor,
freeMonoIconColor: UIColor,
itemSwitchColors: PresentationThemeSwitch,
itemDisclosureActions: PresentationThemeItemDisclosureActions,
itemCheckColors: PresentationThemeFillStrokeForeground,
controlSecondaryColor: UIColor,
freeInputField: PresentationInputFieldTheme,
freePlainInputField: PresentationInputFieldTheme,
mediaPlaceholderColor: UIColor,
scrollIndicatorColor: UIColor,
pageIndicatorInactiveColor: UIColor,
inputClearButtonColor: UIColor,
itemBarChart: PresentationThemeItemBarChart,
itemInputField: PresentationInputFieldTheme,
paymentOption: PaymentOption
) {
self.blocksBackgroundColor = blocksBackgroundColor
self.plainBackgroundColor = plainBackgroundColor
self.itemPrimaryTextColor = itemPrimaryTextColor
@ -470,10 +523,11 @@ public final class PresentationThemeList {
self.inputClearButtonColor = inputClearButtonColor
self.itemBarChart = itemBarChart
self.itemInputField = itemInputField
self.paymentOption = paymentOption
}
public func withUpdated(blocksBackgroundColor: UIColor? = nil, plainBackgroundColor: UIColor? = nil, itemPrimaryTextColor: UIColor? = nil, itemSecondaryTextColor: UIColor? = nil, itemDisabledTextColor: UIColor? = nil, itemAccentColor: UIColor? = nil, itemHighlightedColor: UIColor? = nil, itemDestructiveColor: UIColor? = nil, itemPlaceholderTextColor: UIColor? = nil, itemBlocksBackgroundColor: UIColor? = nil, itemHighlightedBackgroundColor: UIColor? = nil, itemBlocksSeparatorColor: UIColor? = nil, itemPlainSeparatorColor: UIColor? = nil, disclosureArrowColor: UIColor? = nil, sectionHeaderTextColor: UIColor? = nil, freeTextColor: UIColor? = nil, freeTextErrorColor: UIColor? = nil, freeTextSuccessColor: UIColor? = nil, freeMonoIconColor: UIColor? = nil, itemSwitchColors: PresentationThemeSwitch? = nil, itemDisclosureActions: PresentationThemeItemDisclosureActions? = nil, itemCheckColors: PresentationThemeFillStrokeForeground? = nil, controlSecondaryColor: UIColor? = nil, freeInputField: PresentationInputFieldTheme? = nil, freePlainInputField: PresentationInputFieldTheme? = nil, mediaPlaceholderColor: UIColor? = nil, scrollIndicatorColor: UIColor? = nil, pageIndicatorInactiveColor: UIColor? = nil, inputClearButtonColor: UIColor? = nil, itemBarChart: PresentationThemeItemBarChart? = nil, itemInputField: PresentationInputFieldTheme? = nil) -> PresentationThemeList {
return PresentationThemeList(blocksBackgroundColor: blocksBackgroundColor ?? self.blocksBackgroundColor, plainBackgroundColor: plainBackgroundColor ?? self.plainBackgroundColor, itemPrimaryTextColor: itemPrimaryTextColor ?? self.itemPrimaryTextColor, itemSecondaryTextColor: itemSecondaryTextColor ?? self.itemSecondaryTextColor, itemDisabledTextColor: itemDisabledTextColor ?? self.itemDisabledTextColor, itemAccentColor: itemAccentColor ?? self.itemAccentColor, itemHighlightedColor: itemHighlightedColor ?? self.itemHighlightedColor, itemDestructiveColor: itemDestructiveColor ?? self.itemDestructiveColor, itemPlaceholderTextColor: itemPlaceholderTextColor ?? self.itemPlaceholderTextColor, itemBlocksBackgroundColor: itemBlocksBackgroundColor ?? self.itemBlocksBackgroundColor, itemHighlightedBackgroundColor: itemHighlightedBackgroundColor ?? self.itemHighlightedBackgroundColor, itemBlocksSeparatorColor: itemBlocksSeparatorColor ?? self.itemBlocksSeparatorColor, itemPlainSeparatorColor: itemPlainSeparatorColor ?? self.itemPlainSeparatorColor, disclosureArrowColor: disclosureArrowColor ?? self.disclosureArrowColor, sectionHeaderTextColor: sectionHeaderTextColor ?? self.sectionHeaderTextColor, freeTextColor: freeTextColor ?? self.freeTextColor, freeTextErrorColor: freeTextErrorColor ?? self.freeTextErrorColor, freeTextSuccessColor: freeTextSuccessColor ?? self.freeTextSuccessColor, freeMonoIconColor: freeMonoIconColor ?? self.freeMonoIconColor, itemSwitchColors: itemSwitchColors ?? self.itemSwitchColors, itemDisclosureActions: itemDisclosureActions ?? self.itemDisclosureActions, itemCheckColors: itemCheckColors ?? self.itemCheckColors, controlSecondaryColor: controlSecondaryColor ?? self.controlSecondaryColor, freeInputField: freeInputField ?? self.freeInputField, freePlainInputField: freePlainInputField ?? self.freePlainInputField, mediaPlaceholderColor: mediaPlaceholderColor ?? self.mediaPlaceholderColor, scrollIndicatorColor: scrollIndicatorColor ?? self.scrollIndicatorColor, pageIndicatorInactiveColor: pageIndicatorInactiveColor ?? self.pageIndicatorInactiveColor, inputClearButtonColor: inputClearButtonColor ?? self.inputClearButtonColor, itemBarChart: itemBarChart ?? self.itemBarChart, itemInputField: itemInputField ?? self.itemInputField)
public func withUpdated(blocksBackgroundColor: UIColor? = nil, plainBackgroundColor: UIColor? = nil, itemPrimaryTextColor: UIColor? = nil, itemSecondaryTextColor: UIColor? = nil, itemDisabledTextColor: UIColor? = nil, itemAccentColor: UIColor? = nil, itemHighlightedColor: UIColor? = nil, itemDestructiveColor: UIColor? = nil, itemPlaceholderTextColor: UIColor? = nil, itemBlocksBackgroundColor: UIColor? = nil, itemHighlightedBackgroundColor: UIColor? = nil, itemBlocksSeparatorColor: UIColor? = nil, itemPlainSeparatorColor: UIColor? = nil, disclosureArrowColor: UIColor? = nil, sectionHeaderTextColor: UIColor? = nil, freeTextColor: UIColor? = nil, freeTextErrorColor: UIColor? = nil, freeTextSuccessColor: UIColor? = nil, freeMonoIconColor: UIColor? = nil, itemSwitchColors: PresentationThemeSwitch? = nil, itemDisclosureActions: PresentationThemeItemDisclosureActions? = nil, itemCheckColors: PresentationThemeFillStrokeForeground? = nil, controlSecondaryColor: UIColor? = nil, freeInputField: PresentationInputFieldTheme? = nil, freePlainInputField: PresentationInputFieldTheme? = nil, mediaPlaceholderColor: UIColor? = nil, scrollIndicatorColor: UIColor? = nil, pageIndicatorInactiveColor: UIColor? = nil, inputClearButtonColor: UIColor? = nil, itemBarChart: PresentationThemeItemBarChart? = nil, itemInputField: PresentationInputFieldTheme? = nil, paymentOption: PaymentOption? = nil) -> PresentationThemeList {
return PresentationThemeList(blocksBackgroundColor: blocksBackgroundColor ?? self.blocksBackgroundColor, plainBackgroundColor: plainBackgroundColor ?? self.plainBackgroundColor, itemPrimaryTextColor: itemPrimaryTextColor ?? self.itemPrimaryTextColor, itemSecondaryTextColor: itemSecondaryTextColor ?? self.itemSecondaryTextColor, itemDisabledTextColor: itemDisabledTextColor ?? self.itemDisabledTextColor, itemAccentColor: itemAccentColor ?? self.itemAccentColor, itemHighlightedColor: itemHighlightedColor ?? self.itemHighlightedColor, itemDestructiveColor: itemDestructiveColor ?? self.itemDestructiveColor, itemPlaceholderTextColor: itemPlaceholderTextColor ?? self.itemPlaceholderTextColor, itemBlocksBackgroundColor: itemBlocksBackgroundColor ?? self.itemBlocksBackgroundColor, itemHighlightedBackgroundColor: itemHighlightedBackgroundColor ?? self.itemHighlightedBackgroundColor, itemBlocksSeparatorColor: itemBlocksSeparatorColor ?? self.itemBlocksSeparatorColor, itemPlainSeparatorColor: itemPlainSeparatorColor ?? self.itemPlainSeparatorColor, disclosureArrowColor: disclosureArrowColor ?? self.disclosureArrowColor, sectionHeaderTextColor: sectionHeaderTextColor ?? self.sectionHeaderTextColor, freeTextColor: freeTextColor ?? self.freeTextColor, freeTextErrorColor: freeTextErrorColor ?? self.freeTextErrorColor, freeTextSuccessColor: freeTextSuccessColor ?? self.freeTextSuccessColor, freeMonoIconColor: freeMonoIconColor ?? self.freeMonoIconColor, itemSwitchColors: itemSwitchColors ?? self.itemSwitchColors, itemDisclosureActions: itemDisclosureActions ?? self.itemDisclosureActions, itemCheckColors: itemCheckColors ?? self.itemCheckColors, controlSecondaryColor: controlSecondaryColor ?? self.controlSecondaryColor, freeInputField: freeInputField ?? self.freeInputField, freePlainInputField: freePlainInputField ?? self.freePlainInputField, mediaPlaceholderColor: mediaPlaceholderColor ?? self.mediaPlaceholderColor, scrollIndicatorColor: scrollIndicatorColor ?? self.scrollIndicatorColor, pageIndicatorInactiveColor: pageIndicatorInactiveColor ?? self.pageIndicatorInactiveColor, inputClearButtonColor: inputClearButtonColor ?? self.inputClearButtonColor, itemBarChart: itemBarChart ?? self.itemBarChart, itemInputField: itemInputField ?? self.itemInputField, paymentOption: paymentOption ?? self.paymentOption)
}
}

View File

@ -745,6 +745,33 @@ extension PresentationInputFieldTheme: Codable {
}
}
extension PresentationThemeList.PaymentOption: Codable {
enum CodingKeys: String, CodingKey {
case inactiveFill
case inactiveForeground
case activeFill
case activeForeground
}
public convenience init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.init(
inactiveFillColor: try decodeColor(values, .inactiveFill),
inactiveForegroundColor: try decodeColor(values, .inactiveForeground),
activeFillColor: try decodeColor(values, .activeFill),
activeForegroundColor: try decodeColor(values, .activeForeground)
)
}
public func encode(to encoder: Encoder) throws {
var values = encoder.container(keyedBy: CodingKeys.self)
try encodeColor(&values, self.activeFillColor, .inactiveFill)
try encodeColor(&values, self.activeForegroundColor, .inactiveForeground)
try encodeColor(&values, self.activeFillColor, .activeFill)
try encodeColor(&values, self.activeForegroundColor, .activeForeground)
}
}
extension PresentationThemeList: Codable {
enum CodingKeys: String, CodingKey {
case blocksBg
@ -778,6 +805,7 @@ extension PresentationThemeList: Codable {
case inputClearButton
case itemBarChart
case itemInputField
case paymentOption
}
public convenience init(from decoder: Decoder) throws {
@ -789,6 +817,8 @@ extension PresentationThemeList: Codable {
} else {
freePlainInputField = try values.decode(PresentationInputFieldTheme.self, forKey: .freeInputField)
}
let freeTextSuccessColor = try decodeColor(values, .freeTextSuccess)
self.init(
blocksBackgroundColor: try decodeColor(values, .blocksBg),
@ -808,7 +838,7 @@ extension PresentationThemeList: Codable {
sectionHeaderTextColor: try decodeColor(values, .sectionHeaderText),
freeTextColor: try decodeColor(values, .freeText),
freeTextErrorColor: try decodeColor(values, .freeTextError),
freeTextSuccessColor: try decodeColor(values, .freeTextSuccess),
freeTextSuccessColor: freeTextSuccessColor,
freeMonoIconColor: try decodeColor(values, .freeMonoIcon),
itemSwitchColors: try values.decode(PresentationThemeSwitch.self, forKey: .switch),
itemDisclosureActions: try values.decode(PresentationThemeItemDisclosureActions.self, forKey: .disclosureActions),
@ -821,7 +851,13 @@ extension PresentationThemeList: Codable {
pageIndicatorInactiveColor: try decodeColor(values, .pageIndicatorInactive),
inputClearButtonColor: try decodeColor(values, .inputClearButton),
itemBarChart: try values.decode(PresentationThemeItemBarChart.self, forKey: .itemBarChart),
itemInputField: try values.decode(PresentationInputFieldTheme.self, forKey: .itemInputField)
itemInputField: try values.decode(PresentationInputFieldTheme.self, forKey: .itemInputField),
paymentOption: (try? values.decode(PresentationThemeList.PaymentOption.self, forKey: .paymentOption)) ?? PresentationThemeList.PaymentOption(
inactiveFillColor: freeTextSuccessColor.withMultipliedAlpha(0.3),
inactiveForegroundColor: freeTextSuccessColor,
activeFillColor: freeTextSuccessColor,
activeForegroundColor: UIColor(rgb: 0xffffff)
)
)
}

View File

@ -165,7 +165,7 @@ public func formatCurrencyAmount(_ amount: Int64, currency: String) -> String {
}
}
public func formatCurrencyAmountCustom(_ amount: Int64, currency: String) -> (String, String) {
public func formatCurrencyAmountCustom(_ amount: Int64, currency: String) -> (String, String, Bool) {
if let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] {
var result = ""
if amount < 0 {
@ -198,8 +198,8 @@ public func formatCurrencyAmountCustom(_ amount: Int64, currency: String) -> (St
result.append(entry.symbol)
}*/
return (result, entry.symbol)
return (result, entry.symbol, entry.symbolOnLeft)
} else {
return ("", "")
return ("", "", false)
}
}

View File

@ -449,8 +449,14 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
case let .groupPhoneCall(_, _, scheduleDate, duration):
if let scheduleDate = scheduleDate {
let timeString = humanReadableStringForTimestamp(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: scheduleDate)
let titleString = strings.Notification_VoiceChatScheduled(timeString).0
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
if message.author?.id.namespace == Namespaces.Peer.CloudChannel {
let titleString = strings.Notification_VoiceChatScheduledChannel(timeString).0
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
} else {
let attributePeerIds: [(Int, PeerId?)] = [(0, message.author?.id)]
let titleString = strings.Notification_VoiceChatScheduled(authorName, timeString)
attributedString = addAttributesToStringWithRanges(titleString, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: attributePeerIds))
}
} else if let duration = duration {
let titleString = strings.Notification_VoiceChatEnded(callDurationString(strings: strings, value: duration)).0
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)

View File

@ -815,14 +815,14 @@ final class SharedApplicationContext {
}
})
self.mainWindow.debugAction = {
/*self.mainWindow.debugAction = {
self.mainWindow.debugAction = nil
let presentationData = sharedContext.currentPresentationData.with { $0 }
let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme))
navigationController.viewControllers = [debugController(sharedContext: sharedContext, context: nil)]
self.mainWindow.present(navigationController, on: .root)
}
}*/
presentationDataPromise.set(sharedContext.presentationData)

View File

@ -356,6 +356,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private weak var sendMessageActionsController: ChatSendMessageActionSheetController?
private var searchResultsController: ChatSearchResultsController?
private weak var currentPinchController: PinchController?
private weak var currentPinchSourceItemNode: ListViewItemNode?
private var screenCaptureManager: ScreenCaptureDetectionManager?
private let chatAdditionalDataDisposable = MetaDisposable()
@ -579,7 +582,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let text: String
switch error {
case .generic:
case .generic, .scheduledTooLate:
text = strongSelf.presentationData.strings.Login_UnknownError
case .anonymousNotAllowed:
text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText
@ -624,12 +627,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.presentAutoremoveSetup()
}
case .paymentSent:
for attribute in message.attributes {
strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: message.id), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
/*for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId))
//strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId))
break
}
}
}*/
return true
default:
break
@ -920,7 +924,26 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self else {
return
}
let pinchController = PinchController(sourceNode: sourceNode)
var sourceItemNode: ListViewItemNode?
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
guard let itemNode = itemNode as? ListViewItemNode else {
return
}
if sourceNode.view.isDescendant(of: itemNode.view) {
sourceItemNode = itemNode
}
}
let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: {
guard let strongSelf = self else {
return CGRect()
}
return strongSelf.chatDisplayNode.view.convert(strongSelf.chatDisplayNode.frameForVisibleArea(), to: nil)
})
strongSelf.currentPinchController = pinchController
strongSelf.currentPinchSourceItemNode = sourceItemNode
strongSelf.window?.presentInGlobalOverlay(pinchController)
}, openMessageContextActions: { message, node, rect, gesture in
gesture?.cancel()
@ -1848,7 +1871,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let invoice = media as? TelegramMediaInvoice {
strongSelf.chatDisplayNode.dismissInput()
if let receiptMessageId = invoice.receiptMessageId {
strongSelf.present(BotReceiptController(context: strongSelf.context, invoice: invoice, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} else {
strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, messageId: messageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
@ -4011,21 +4034,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self else {
return
}
for (tooltipScreen, tooltipItemNode) in strongSelf.currentMessageTooltipScreens {
if let itemNode = itemNode {
if itemNode === tooltipItemNode {
tooltipScreen.addRelativeScrollingOffset(-offset, transition: transition)
}
} else {
tooltipScreen.addRelativeScrollingOffset(-offset, transition: transition)
}
}
}
self.chatDisplayNode.historyNode.didScrollWithOffset = { [weak self] offset, _, _ in
guard let strongSelf = self else {
return
}
if offset > 0.0 {
if var scrolledToMessageIdValue = strongSelf.scrolledToMessageIdValue {
@ -4035,6 +4043,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else if offset < 0.0 {
strongSelf.scrolledToMessageIdValue = nil
}
if let currentPinchSourceItemNode = strongSelf.currentPinchSourceItemNode {
if let itemNode = itemNode {
if itemNode === currentPinchSourceItemNode {
strongSelf.currentPinchController?.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition)
}
} else {
strongSelf.currentPinchController?.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition)
}
}
}
if case .pinnedMessages = self.presentationInterfaceState.subject {

View File

@ -143,7 +143,6 @@ class ChatMessageShareButton: HighlightableButtonNode {
class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private let contextSourceNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let pinchContainerNode: PinchSourceContainerNode
let imageNode: TransformImageNode
private var placeholderNode: StickerShimmerEffectNode
private var animationNode: GenericAnimatedStickerNode?
@ -196,7 +195,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
required init() {
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.pinchContainerNode = PinchSourceContainerNode()
self.imageNode = TransformImageNode()
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
@ -264,8 +262,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
self.imageNode.displaysAsynchronously = false
self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.pinchContainerNode.contentNode.addSubnode(self.containerNode)
self.addSubnode(self.pinchContainerNode)
self.addSubnode(self.containerNode)
self.contextSourceNode.contentNode.addSubnode(self.imageNode)
self.contextSourceNode.contentNode.addSubnode(self.placeholderNode)
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
@ -281,23 +278,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
item.controllerInteraction.openMessageReactions(item.message.id)
}
self.pinchContainerNode.activate = { [weak self] sourceNode in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.activateMessagePinch(sourceNode)
}
self.pinchContainerNode.scaleUpdated = { [weak self] scale, transition in
guard let strongSelf = self else {
return
}
let factor: CGFloat = max(0.0, min(1.0, (scale - 1.0) * 8.0))
transition.updateAlpha(node: strongSelf.dateAndStatusNode, alpha: 1.0 - factor)
}
}
deinit {
@ -996,8 +976,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.pinchContainerNode.update(size: layoutSize, transition: .immediate)
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
@ -1085,7 +1063,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
imageApply()
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
strongSelf.pinchContainerNode.contentRect = strongSelf.imageNode.frame
strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect
if let updatedShareButtonNode = updatedShareButtonNode {

View File

@ -926,7 +926,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
}
videoNode.updateLayout(size: arguments.drawingSize, transition: .immediate)
videoNode.frame = imageFrame
videoNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size)
if strongSelf.visibility {
if !videoNode.canAttachContent {

View File

@ -21,7 +21,6 @@ private let inlineBotNameFont = nameFont
class ChatMessageStickerItemNode: ChatMessageItemView {
private let contextSourceNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let pinchContainerNode: PinchSourceContainerNode
let imageNode: TransformImageNode
private var placeholderNode: StickerShimmerEffectNode
var textNode: TextNode?
@ -54,7 +53,6 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
required init() {
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.pinchContainerNode = PinchSourceContainerNode()
self.imageNode = TransformImageNode()
self.placeholderNode = StickerShimmerEffectNode()
self.placeholderNode.isUserInteractionEnabled = false
@ -121,8 +119,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
self.imageNode.displaysAsynchronously = false
self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.pinchContainerNode.contentNode.addSubnode(self.containerNode)
self.addSubnode(self.pinchContainerNode)
self.addSubnode(self.containerNode)
self.contextSourceNode.contentNode.addSubnode(self.placeholderNode)
self.contextSourceNode.contentNode.addSubnode(self.imageNode)
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
@ -138,23 +135,6 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
}
item.controllerInteraction.openMessageReactions(item.message.id)
}
self.pinchContainerNode.activate = { [weak self] sourceNode in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.activateMessagePinch(sourceNode)
}
self.pinchContainerNode.scaleUpdated = { [weak self] scale, transition in
guard let strongSelf = self else {
return
}
let factor: CGFloat = max(0.0, min(1.0, (scale - 1.0) * 8.0))
transition.updateAlpha(node: strongSelf.dateAndStatusNode, alpha: 1.0 - factor)
}
}
required init?(coder aDecoder: NSCoder) {
@ -675,12 +655,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.pinchContainerNode.update(size: layoutSize, transition: .immediate)
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
strongSelf.pinchContainerNode.contentRect = strongSelf.imageNode.frame
strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect
dateAndStatusApply(false)

View File

@ -4029,7 +4029,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
let text: String
switch error {
case .generic:
case .generic, .scheduledTooLate:
text = strongSelf.presentationData.strings.Login_UnknownError
case .anonymousNotAllowed:
text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText

View File

@ -16,6 +16,7 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry {
public var enableVoipTcp: Bool
public var demoVideoChats: Bool
public var experimentalCompatibility: Bool
public var enableNoiseSuppression: Bool
public static var defaultSettings: ExperimentalUISettings {
return ExperimentalUISettings(
@ -31,7 +32,8 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry {
disableVideoAspectScaling: false,
enableVoipTcp: false,
demoVideoChats: false,
experimentalCompatibility: false
experimentalCompatibility: false,
enableNoiseSuppression: false
)
}
@ -48,7 +50,8 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry {
disableVideoAspectScaling: Bool,
enableVoipTcp: Bool,
demoVideoChats: Bool,
experimentalCompatibility: Bool
experimentalCompatibility: Bool,
enableNoiseSuppression: Bool
) {
self.keepChatNavigationStack = keepChatNavigationStack
self.skipReadHistory = skipReadHistory
@ -63,6 +66,7 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry {
self.enableVoipTcp = enableVoipTcp
self.demoVideoChats = demoVideoChats
self.experimentalCompatibility = experimentalCompatibility
self.enableNoiseSuppression = enableNoiseSuppression
}
public init(decoder: PostboxDecoder) {
@ -79,6 +83,7 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry {
self.enableVoipTcp = decoder.decodeInt32ForKey("enableVoipTcp", orElse: 0) != 0
self.demoVideoChats = decoder.decodeInt32ForKey("demoVideoChats", orElse: 0) != 0
self.experimentalCompatibility = decoder.decodeInt32ForKey("experimentalCompatibility", orElse: 0) != 0
self.enableNoiseSuppression = decoder.decodeInt32ForKey("enableNoiseSuppression", orElse: 0) != 0
}
public func encode(_ encoder: PostboxEncoder) {
@ -97,6 +102,7 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry {
encoder.encodeInt32(self.enableVoipTcp ? 1 : 0, forKey: "enableVoipTcp")
encoder.encodeInt32(self.demoVideoChats ? 1 : 0, forKey: "demoVideoChats")
encoder.encodeInt32(self.experimentalCompatibility ? 1 : 0, forKey: "experimentalCompatibility")
encoder.encodeInt32(self.enableNoiseSuppression ? 1 : 0, forKey: "enableNoiseSuppression")
}
public func isEqual(to: PreferencesEntry) -> Bool {

View File

@ -180,7 +180,7 @@ public final class OngoingGroupCallContext {
private var broadcastPartsSource: BroadcastPartSource?
init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?, participantDescriptionsRequired: @escaping (Set<UInt32>) -> Void, audioStreamData: AudioStreamData?, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, enableVideo: Bool) {
init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?, participantDescriptionsRequired: @escaping (Set<UInt32>) -> Void, audioStreamData: AudioStreamData?, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, enableVideo: Bool, enableNoiseSuppression: Bool) {
self.queue = queue
var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)?
@ -224,7 +224,7 @@ public final class OngoingGroupCallContext {
},
outgoingAudioBitrateKbit: outgoingAudioBitrateKbit ?? 32,
enableVideo: enableVideo,
enableNoiseSuppression: true
enableNoiseSuppression: enableNoiseSuppression
)
let queue = self.queue
@ -529,10 +529,10 @@ public final class OngoingGroupCallContext {
}
}
public init(inputDeviceId: String = "", outputDeviceId: String = "", video: OngoingCallVideoCapturer?, participantDescriptionsRequired: @escaping (Set<UInt32>) -> Void, audioStreamData: AudioStreamData?, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, enableVideo: Bool) {
public init(inputDeviceId: String = "", outputDeviceId: String = "", video: OngoingCallVideoCapturer?, participantDescriptionsRequired: @escaping (Set<UInt32>) -> Void, audioStreamData: AudioStreamData?, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, enableVideo: Bool, enableNoiseSuppression: Bool) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, video: video, participantDescriptionsRequired: participantDescriptionsRequired, audioStreamData: audioStreamData, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: enableVideo)
return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, video: video, participantDescriptionsRequired: participantDescriptionsRequired, audioStreamData: audioStreamData, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: enableVideo, enableNoiseSuppression: enableNoiseSuppression)
})
}

View File

@ -1,5 +1,5 @@
{
"app": "7.6.2",
"app": "7.7",
"bazel": "4.0.0",
"xcode": "12.4"
}