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 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 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=$(git rev-list --count HEAD)
export COMMIT_COUNT="$(($COMMIT_COUNT+2000))" export COMMIT_COUNT="$(($COMMIT_COUNT+$BUILD_NUMBER_OFFSET))"
export BUILD_NUMBER="$COMMIT_COUNT" export BUILD_NUMBER="$COMMIT_COUNT"
echo "BUILD_NUMBER=$(echo $BUILD_NUMBER)" >> $GITHUB_ENV echo "BUILD_NUMBER=$(echo $BUILD_NUMBER)" >> $GITHUB_ENV
echo "APP_VERSION=$(echo $APP_VERSION)" >> $GITHUB_ENV echo "APP_VERSION=$(echo $APP_VERSION)" >> $GITHUB_ENV

View File

@ -70,6 +70,7 @@ beta_testflight:
stage: build stage: build
only: only:
- beta - beta
- hotfix
except: except:
- tags - tags
script: script:
@ -87,6 +88,7 @@ deploy_beta_testflight:
stage: deploy stage: deploy
only: only:
- beta - beta
- hotfix
except: except:
- tags - tags
script: script:
@ -100,6 +102,7 @@ verifysanity_beta_testflight:
stage: verifysanity stage: verifysanity
only: only:
- beta - beta
- hotfix
except: except:
- tags - tags
script: script:
@ -118,6 +121,7 @@ verify_beta_testflight:
stage: verify stage: verify
only: only:
- beta - beta
- hotfix
except: except:
- tags - tags
script: 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.PinVideo" = "Pin Video";
"VoiceChat.UnpinVideo" = "Unpin 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.StartsIn" = "Starts in";
"VoiceChat.LateBy" = "Late by"; "VoiceChat.LateBy" = "Late by";
"VoiceChat.StatusStartsIn" = "starts in %@";
"VoiceChat.StatusLateBy" = "late by %@";
"VoiceChat.StartNow" = "Start Now"; "VoiceChat.StartNow" = "Start Now";
"VoiceChat.SetReminder" = "Set Reminder"; "VoiceChat.SetReminder" = "Set Reminder";
"VoiceChat.CancelReminder" = "Cancel 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') COMMIT_AUTHOR=$(git log -1 --pretty=format:'%an')
if [ -z "$2" ]; then if [ -z "$2" ]; then
COMMIT_COUNT=$(git rev-list --count HEAD) 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" BUILD_NUMBER="$COMMIT_COUNT"
else else
BUILD_NUMBER="$2" BUILD_NUMBER="$2"

View File

@ -43,7 +43,7 @@ enum BotCheckoutActionButtonState: Equatable {
private let titleFont = Font.semibold(17.0) private let titleFont = Font.semibold(17.0)
final class BotCheckoutActionButton: HighlightableButtonNode { final class BotCheckoutActionButton: HighlightableButtonNode {
static var diameter: CGFloat = 48.0 static var height: CGFloat = 52.0
private var inactiveFillColor: UIColor private var inactiveFillColor: UIColor
private var activeFillColor: UIColor private var activeFillColor: UIColor
@ -63,11 +63,13 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
self.activeFillColor = activeFillColor self.activeFillColor = activeFillColor
self.foregroundColor = foregroundColor self.foregroundColor = foregroundColor
let diameter: CGFloat = 20.0
self.progressBackgroundNode = ASImageNode() self.progressBackgroundNode = ASImageNode()
self.progressBackgroundNode.displaysAsynchronously = false self.progressBackgroundNode.displaysAsynchronously = false
self.progressBackgroundNode.displayWithoutProcessing = true self.progressBackgroundNode.displayWithoutProcessing = true
self.progressBackgroundNode.isLayerBacked = 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)) context.clear(CGRect(origin: CGPoint(), size: size))
let strokeWidth: CGFloat = 2.0 let strokeWidth: CGFloat = 2.0
context.setFillColor(activeFillColor.cgColor) context.setFillColor(activeFillColor.cgColor)
@ -75,7 +77,7 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
context.setFillColor(inactiveFillColor.cgColor) 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))) 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))) 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.displaysAsynchronously = false
self.inactiveBackgroundNode.displayWithoutProcessing = true self.inactiveBackgroundNode.displayWithoutProcessing = true
self.inactiveBackgroundNode.isLayerBacked = 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.inactiveBackgroundNode.alpha = 0.0
self.activeBackgroundNode = ASImageNode() self.activeBackgroundNode = ASImageNode()
self.activeBackgroundNode.displaysAsynchronously = false self.activeBackgroundNode.displaysAsynchronously = false
self.activeBackgroundNode.displayWithoutProcessing = true self.activeBackgroundNode.displayWithoutProcessing = true
self.activeBackgroundNode.isLayerBacked = 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 = TextNode()
self.labelNode.displaysAsynchronously = false 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) self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
} }
case .applePay: case .applePay:
if case .applePay = previousState { 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 { } 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 { } else {
@ -227,14 +240,18 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
} }
} }
@objc private func applePayButtonPressed() {
self.sendActions(forControlEvents: .touchUpInside, with: nil)
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size 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.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.diameter))) 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.diameter))) 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 { 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 var labelSize = self.labelNode.bounds.size

View File

@ -55,7 +55,7 @@ enum BotCheckoutEntry: ItemListNodeEntry {
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .header: case .header:
return BotCheckoutSection.header.rawValue return BotCheckoutSection.prices.rawValue
case .price, .tip: case .price, .tip:
return BotCheckoutSection.prices.rawValue return BotCheckoutSection.prices.rawValue
default: default:
@ -286,7 +286,7 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st
var index = 0 var index = 0
for price in paymentForm.invoice.prices { for price in paymentForm.invoice.prices {
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false, false)) entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false, index == 0))
totalPrice += price.amount totalPrice += price.amount
index += 1 index += 1
} }
@ -448,6 +448,8 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
private var currentTipAmount: Int64? private var currentTipAmount: Int64?
private var formRequestDisposable: Disposable? private var formRequestDisposable: Disposable?
private let actionButtonPanelNode: ASDisplayNode
private let actionButtonPanelSeparator: ASDisplayNode
private let actionButton: BotCheckoutActionButton private let actionButton: BotCheckoutActionButton
private let inProgressDimNode: ASDisplayNode 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)) 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 |> 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)) 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 = 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 = ASDisplayNode()
self.inProgressDimNode.alpha = 0.0 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 { if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo {
strongSelf.currentPaymentMethod = method strongSelf.currentPaymentMethod = method
strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount))) strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount)))
strongSelf.updateActionButton()
} }
} }
@ -551,7 +561,75 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
var dismissImpl: (() -> Void)? var dismissImpl: (() -> Void)?
let canSave = paymentForm.canSaveCredentials || paymentForm.passwordMissing 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
}
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 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 { guard let strongSelf = self else {
return return
} }
@ -754,9 +832,12 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
}) })
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.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside)
self.actionButton.isEnabled = false self.actionButton.isEnabled = false
self.addSubnode(self.actionButton)
self.listNode.supernode?.insertSubnode(self.inProgressDimNode, aboveSubnode: self.listNode) self.listNode.supernode?.insertSubnode(self.inProgressDimNode, aboveSubnode: self.listNode)
} }
@ -775,22 +856,37 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
} else { } else {
payString = self.presentationData.strings.CheckoutInfo_Pay payString = self.presentationData.strings.CheckoutInfo_Pay
} }
if self.actionButton.isEnabled { if let currentPaymentMethod = self.currentPaymentMethod {
switch currentPaymentMethod {
case .applePay:
self.actionButton.setState(.applePay)
default:
self.actionButton.setState(.active(payString)) self.actionButton.setState(.active(payString))
} else {
self.actionButton.setState(.loading)
} }
} else {
self.actionButton.setState(.active(payString))
}
self.actionButtonPanelNode.isHidden = false
} }
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) { override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) {
var updatedInsets = layout.intrinsicInsets 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) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
self.actionButton.updateLayout(size: actionButtonFrame.size, transition: transition) 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) transition.updateFrame(node: self.inProgressDimNode, frame: self.listNode.frame)
} }

View File

@ -80,7 +80,6 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
init() { init() {
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode() self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true self.topStripeNode.isLayerBacked = true
@ -110,6 +109,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
super.init(layerBacked: false, dynamicBounce: false) super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.imageNode) self.addSubnode(self.imageNode)
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
self.addSubnode(self.textNode) 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) 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() strongSelf.backgroundNode.removeFromSupernode()
} }*/
if strongSelf.topStripeNode.supernode != nil { if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode() strongSelf.topStripeNode.removeFromSupernode()
} }
@ -232,6 +232,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
strongSelf.botNameNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + textBotNameSpacing), size: botNameLayout.size) 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)) 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 { final class BotCheckoutNativeCardEntryController: ViewController {
enum Provider {
case stripe(additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String)
case smartglobal(isTesting: Bool, publicToken: String)
}
private var controllerNode: BotCheckoutNativeCardEntryControllerNode { private var controllerNode: BotCheckoutNativeCardEntryControllerNode {
return super.displayNode as! BotCheckoutNativeCardEntryControllerNode return super.displayNode as! BotCheckoutNativeCardEntryControllerNode
} }
private let context: AccountContext private let context: AccountContext
private let additionalFields: BotCheckoutNativeCardEntryAdditionalFields private let provider: Provider
private let publishableKey: String
private let completion: (BotCheckoutPaymentMethod) -> Void private let completion: (BotCheckoutPaymentMethod) -> Void
private var presentationData: PresentationData private var presentationData: PresentationData
@ -46,10 +50,9 @@ final class BotCheckoutNativeCardEntryController: ViewController {
private var doneItem: UIBarButtonItem? private var doneItem: UIBarButtonItem?
private var activityItem: 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.context = context
self.additionalFields = additionalFields self.provider = provider
self.publishableKey = publishableKey
self.completion = completion self.completion = completion
self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -71,7 +74,7 @@ final class BotCheckoutNativeCardEntryController: ViewController {
} }
override public func loadDisplayNode() { 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) self?.present(c, in: .window(.root), with: a)
}, dismiss: { [weak self] in }, dismiss: { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil) self?.presentingViewController?.dismiss(animated: false, completion: nil)

View File

@ -42,7 +42,7 @@ private final class BotCheckoutNativeCardEntryScrollerNode: ASDisplayNode {
} }
final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, UIScrollViewDelegate {
private let publishableKey: String private let provider: BotCheckoutNativeCardEntryController.Provider
private let present: (ViewController, Any?) -> Void private let present: (ViewController, Any?) -> Void
private let dismiss: () -> Void private let dismiss: () -> Void
@ -71,8 +71,10 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
private var currentCardData: BotPaymentCardInputData? private var currentCardData: BotPaymentCardInputData?
private var currentCountryIso2: String? private var currentCountryIso2: String?
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) { private var dataTask: URLSessionDataTask?
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.present = present
self.dismiss = dismiss self.dismiss = dismiss
@ -96,6 +98,8 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
} }
itemNodes.append([BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PaymentCard), self.cardItem]) itemNodes.append([BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PaymentCard), self.cardItem])
switch provider {
case let .stripe(additionalFields, _):
if additionalFields.contains(.cardholderName) { if additionalFields.contains(.cardholderName) {
var sectionItems: [BotPaymentItemNode] = [] var sectionItems: [BotPaymentItemNode] = []
@ -138,6 +142,11 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
self.countryItem = nil self.countryItem = nil
self.zipCodeItem = nil self.zipCodeItem = nil
} }
case .smartglobal:
self.cardholderItem = nil
self.countryItem = nil
self.zipCodeItem = nil
}
self.saveInfoItem = BotPaymentSwitchItemNode(title: strings.Checkout_NewCard_SaveInfo, isOn: false) self.saveInfoItem = BotPaymentSwitchItemNode(title: strings.Checkout_NewCard_SaveInfo, isOn: false)
itemNodes.append([self.saveInfoItem, BotPaymentTextItemNode(text: strings.Checkout_NewCard_SaveInfoHelp)]) itemNodes.append([self.saveInfoItem, BotPaymentTextItemNode(text: strings.Checkout_NewCard_SaveInfoHelp)])
@ -214,6 +223,7 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
deinit { deinit {
self.verifyDisposable.dispose() self.verifyDisposable.dispose()
self.dataTask?.cancel()
} }
func updateCountry(_ iso2: String) { func updateCountry(_ iso2: String) {
@ -233,9 +243,11 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
return return
} }
switch self.provider {
case let .stripe(_, publishableKey):
let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration
configuration.smsAutofillDisabled = true configuration.smsAutofillDisabled = true
configuration.publishableKey = self.publishableKey configuration.publishableKey = publishableKey
configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph" configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph"
let apiClient = STPAPIClient(configuration: configuration) let apiClient = STPAPIClient(configuration: configuration)
@ -279,6 +291,100 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
})) }))
self.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()
}
}
})
self.dataTask = dataTask
self.isVerifying = true
self.updateDone()
dataTask.resume()
break
}
} }
private func 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) { 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 { async {
let node = BotCheckoutPriceItemNode() 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.contentSize = layout.contentSize
node.insets = layout.insets node.insets = layout.insets
@ -48,7 +48,7 @@ class BotCheckoutPriceItem: ListViewItem, ItemListItem {
let makeLayout = nodeValue.asyncLayout() let makeLayout = nodeValue.asyncLayout()
async { 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 { Queue.mainQueue().async {
completion(layout, { _ in completion(layout, { _ in
apply() apply()
@ -69,13 +69,13 @@ private func priceItemInsets(_ neighbors: ItemListNeighbors) -> UIEdgeInsets {
var insets = UIEdgeInsets() var insets = UIEdgeInsets()
switch neighbors.top { switch neighbors.top {
case .otherSection: case .otherSection:
insets.top += 8.0 insets.top += 24.0
case .none, .sameSection: case .none, .sameSection:
break break
} }
switch neighbors.bottom { switch neighbors.bottom {
case .none, .otherSection: case .none, .otherSection:
insets.bottom += 8.0 insets.bottom += 24.0
case .sameSection: case .sameSection:
break break
} }
@ -86,9 +86,9 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
let titleNode: TextNode let titleNode: TextNode
let labelNode: TextNode let labelNode: TextNode
let backgroundNode: ASDisplayNode
let separatorNode: ASDisplayNode let separatorNode: ASDisplayNode
let bottomSeparatorNode: ASDisplayNode let bottomSeparatorNode: ASDisplayNode
let spacerNode: ASDisplayNode
private var item: BotCheckoutPriceItem? private var item: BotCheckoutPriceItem?
@ -99,37 +99,44 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
self.labelNode = TextNode() self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false self.labelNode.isUserInteractionEnabled = false
self.backgroundNode = ASDisplayNode()
self.separatorNode = ASDisplayNode() self.separatorNode = ASDisplayNode()
self.bottomSeparatorNode = ASDisplayNode() self.bottomSeparatorNode = ASDisplayNode()
self.spacerNode = ASDisplayNode()
super.init(layerBacked: false, dynamicBounce: false) super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.spacerNode) self.addSubnode(self.backgroundNode)
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode) self.addSubnode(self.labelNode)
self.addSubnode(self.separatorNode) self.addSubnode(self.separatorNode)
self.addSubnode(self.bottomSeparatorNode) 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 makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode) 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 rightInset: CGFloat = 16.0 + params.rightInset
let naturalContentHeight: CGFloat = 34.0 let naturalContentHeight: CGFloat
var verticalOffset: CGFloat = 0.0
var contentSize = CGSize(width: params.width, height: naturalContentHeight)
var insets = priceItemInsets(neighbors)
if item.hasSeparator {
insets.top += 5.0
}
if item.isFinal { 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 textFont: UIFont
let textColor: UIColor let textColor: UIColor
@ -154,21 +161,15 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
let leftInset: CGFloat = 16.0 + params.leftInset let leftInset: CGFloat = 16.0 + params.leftInset
strongSelf.separatorNode.isHidden = !item.hasSeparator 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.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.isHidden = !item.isFinal
strongSelf.bottomSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor strongSelf.bottomSeparatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: naturalContentHeight + 10.0), size: CGSize(width: params.width, height: UIScreenPixel)) strongSelf.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: CGSize(width: params.width, height: UIScreenPixel))
strongSelf.spacerNode.isHidden = !item.isFinal strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.spacerNode.backgroundColor = item.theme.list.blocksBackgroundColor strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: contentSize.height))
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.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset + floor((naturalContentHeight - titleLayout.size.height) / 2.0)), size: titleLayout.size) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset + floor((naturalContentHeight - titleLayout.size.height) / 2.0)), size: titleLayout.size)
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: verticalOffset + floor((naturalContentHeight - labelLayout.size.height) / 2.0)), size: labelLayout.size) strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: verticalOffset + floor((naturalContentHeight - labelLayout.size.height) / 2.0)), size: labelLayout.size)

View File

@ -122,7 +122,7 @@ private final class TipValueNode: ASDisplayNode {
func update(theme: PresentationTheme, text: String, isHighlighted: Bool, height: CGFloat) -> (CGFloat, (CGFloat) -> Void) { func update(theme: PresentationTheme, text: String, isHighlighted: Bool, height: CGFloat) -> (CGFloat, (CGFloat) -> Void) {
var updateBackground = false 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 let currentBackgroundColor = self.currentBackgroundColor {
if !currentBackgroundColor.isEqual(backgroundColor) { if !currentBackgroundColor.isEqual(backgroundColor) {
updateBackground = true updateBackground = true
@ -135,7 +135,7 @@ private final class TipValueNode: ASDisplayNode {
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: backgroundColor) 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 titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: height))
let minWidth: CGFloat = 80.0 let minWidth: CGFloat = 80.0
@ -154,20 +154,23 @@ private final class TipValueNode: ASDisplayNode {
} }
class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate { class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
private let backgroundNode: ASDisplayNode
let titleNode: TextNode let titleNode: TextNode
let labelNode: TextNode let labelNode: TextNode
let tipMeasurementNode: ImmediateTextNode let tipMeasurementNode: ImmediateTextNode
let tipCurrencyNode: ImmediateTextNode let tipCurrencyNode: ImmediateTextNode
private let textNode: TextFieldNode private let textNode: TextFieldNode
private var formatterDelegate: CurrencyUITextFieldDelegate?
private let scrollNode: ASScrollNode private let scrollNode: ASScrollNode
private var valueNodes: [TipValueNode] = [] private var valueNodes: [TipValueNode] = []
private var item: BotCheckoutTipItem? private var item: BotCheckoutTipItem?
private var formatterDelegate: CurrencyUITextFieldDelegate?
init() { init() {
self.backgroundNode = ASDisplayNode()
self.titleNode = TextNode() self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false self.titleNode.isUserInteractionEnabled = false
@ -192,6 +195,8 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
super.init(layerBacked: false, dynamicBounce: false) super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode) self.addSubnode(self.labelNode)
self.addSubnode(self.textNode) self.addSubnode(self.textNode)
@ -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 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 { if strongSelf.textNode.textField.text ?? "" != currencyText.0 {
strongSelf.textNode.textField.text = currencyText.0 strongSelf.textNode.textField.text = currencyText.0
strongSelf.labelNode.isHidden = !currencyText.0.isEmpty 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) strongSelf.tipMeasurementNode.attributedText = NSAttributedString(string: currencyText.0, font: titleFont, textColor: textColor)
let inputTextSize = strongSelf.tipMeasurementNode.updateLayout(textInputFrame.size) let inputTextSize = strongSelf.tipMeasurementNode.updateLayout(textInputFrame.size)
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) strongSelf.tipCurrencyNode.attributedText = NSAttributedString(string: "\(currencyText.1)", font: titleFont, textColor: textColor)
let currencySize = strongSelf.tipCurrencyNode.updateLayout(CGSize(width: 100.0, height: .greatestFiniteMagnitude)) let currencySize = strongSelf.tipCurrencyNode.updateLayout(CGSize(width: 100.0, height: .greatestFiniteMagnitude))
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) 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 textInputFrame.origin.x -= currencySize.width + spaceRect.width
}
strongSelf.textNode.frame = textInputFrame 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.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.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 { if value > item.maxValue {
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 { if self.textNode.textField.text ?? "" != currencyText.0 {
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) { @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) { @objc public func textFieldDidEndEditing(_ textField: UITextField) {

View File

@ -117,7 +117,7 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate {
textInset = max(measuredInset, textInset) 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 return 44.0
} }

View File

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

View File

@ -28,7 +28,7 @@ private enum BotReceiptSection: Int32 {
enum BotReceiptEntry: ItemListNodeEntry { enum BotReceiptEntry: ItemListNodeEntry {
case header(PresentationTheme, TelegramMediaInvoice, String) 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 paymentMethod(PresentationTheme, String, String)
case shippingInfo(PresentationTheme, String, String) case shippingInfo(PresentationTheme, String, String)
case shippingMethod(PresentationTheme, String, String) case shippingMethod(PresentationTheme, String, String)
@ -39,7 +39,7 @@ enum BotReceiptEntry: ItemListNodeEntry {
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .header: case .header:
return BotReceiptSection.header.rawValue return BotReceiptSection.prices.rawValue
case .price: case .price:
return BotReceiptSection.prices.rawValue return BotReceiptSection.prices.rawValue
default: default:
@ -51,7 +51,7 @@ enum BotReceiptEntry: ItemListNodeEntry {
switch self { switch self {
case .header: case .header:
return 0 return 0
case let .price(index, _, _, _, _): case let .price(index, _, _, _, _, _):
return 1 + Int32(index) return 1 + Int32(index)
case .paymentMethod: case .paymentMethod:
return 10000 + 0 return 10000 + 0
@ -85,8 +85,8 @@ enum BotReceiptEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsFinal): case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsHasSeparator, lhsFinal):
if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsFinal) = rhs { if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsHasSeparator, rhsFinal) = rhs {
if lhsIndex != rhsIndex { if lhsIndex != rhsIndex {
return false return false
} }
@ -99,6 +99,9 @@ enum BotReceiptEntry: ItemListNodeEntry {
if lhsValue != rhsValue { if lhsValue != rhsValue {
return false return false
} }
if lhsHasSeparator != rhsHasSeparator {
return false
}
if lhsFinal != rhsFinal { if lhsFinal != rhsFinal {
return false return false
} }
@ -154,8 +157,8 @@ enum BotReceiptEntry: ItemListNodeEntry {
switch self { switch self {
case let .header(theme, invoice, botName): case let .header(theme, invoice, botName):
return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section) return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section)
case let .price(_, theme, text, value, isFinal): case let .price(_, theme, text, value, hasSeparator, isFinal):
return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: false, sectionId: self.section) return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: hasSeparator, sectionId: self.section)
case let .paymentMethod(_, text, value): case let .paymentMethod(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil)
case let .shippingInfo(_, text, value): case let .shippingInfo(_, text, value):
@ -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 entries: [BotReceiptEntry] = []
var botName = "" var botName = ""
if let botPeer = botPeer { if let botPeer = botPeer {
botName = botPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) botName = botPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
} }
if let invoice = invoice {
entries.append(.header(presentationData.theme, invoice, botName)) entries.append(.header(presentationData.theme, invoice, botName))
}
if let formInvoice = formInvoice { if let formInvoice = formInvoice {
var totalPrice: Int64 = 0 var totalPrice: Int64 = 0
var index = 0 var index = 0
for price in formInvoice.prices { 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 totalPrice += price.amount
index += 1 index += 1
} }
@ -196,13 +201,20 @@ private func botReceiptControllerEntries(presentationData: PresentationData, inv
shippingOptionString = shippingOption.title shippingOptionString = shippingOption.title
for price in shippingOption.prices { 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 totalPrice += price.amount
index += 1 index += 1
} }
} }
entries.append(.price(index, presentationData.theme, presentationData.strings.Checkout_TotalAmount, formatCurrencyAmount(totalPrice, currency: formInvoice.currency), true)) 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, true))
if let paymentMethodTitle = paymentMethodTitle { if let paymentMethodTitle = paymentMethodTitle {
entries.append(.paymentMethod(presentationData.theme, presentationData.strings.Checkout_PaymentMethod, paymentMethodTitle)) entries.append(.paymentMethod(presentationData.theme, presentationData.strings.Checkout_PaymentMethod, paymentMethodTitle))
@ -262,12 +274,12 @@ final class BotReceiptControllerNode: ItemListControllerNode {
private var presentationData: PresentationData 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 var dataRequestDisposable: Disposable?
private let actionButton: BotCheckoutActionButton 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.context = context
self.dismissAnimated = dismissAnimated 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)) 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 |> 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)) 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 = 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) 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 self.dataRequestDisposable = (requestBotPaymentReceipt(account: context.account, messageId: messageId) |> deliverOnMainQueue).start(next: { [weak self] receipt in
if let strongSelf = self { 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) { override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) {
var updatedInsets = layout.intrinsicInsets 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) 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) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
self.actionButton.updateLayout(size: actionButtonFrame.size, transition: transition) self.actionButton.updateLayout(size: actionButtonFrame.size, transition: transition)
} }

View File

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

View File

@ -30,15 +30,18 @@ final class PinchSourceGesture: UIPinchGestureRecognizer {
private let target: Target private let target: Target
private(set) var currentTransform: (CGFloat, CGPoint)? private(set) var currentTransform: (CGFloat, CGPoint, CGPoint)?
var began: (() -> Void)? var began: (() -> Void)?
var updated: ((CGFloat, CGPoint) -> Void)? var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
var ended: (() -> Void)? var ended: (() -> Void)?
private var lastLocation: CGPoint? private var initialLocation: CGPoint?
private var pinchLocation = CGPoint()
private var currentOffset = CGPoint() private var currentOffset = CGPoint()
private var currentNumberOfTouches = 0
init() { init() {
self.target = Target() self.target = Target()
@ -52,11 +55,14 @@ final class PinchSourceGesture: UIPinchGestureRecognizer {
override func reset() { override func reset() {
super.reset() super.reset()
self.lastLocation = nil self.currentNumberOfTouches = 0
self.initialLocation = nil
} }
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) { override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event) super.touchesBegan(touches, with: event)
//self.currentTouches.formUnion(touches)
} }
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) { 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) { override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event) 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() { private func gestureUpdated() {
switch self.state { switch self.state {
case .began: case .began:
self.lastLocation = nil
self.currentOffset = CGPoint() 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?() self.began?()
case .changed: 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) let scale = max(1.0, self.scale)
self.currentTransform = (scale, self.currentOffset) self.currentTransform = (scale, self.pinchLocation, self.currentOffset)
self.updated?(scale, self.currentOffset) self.updated?(scale, self.pinchLocation, self.currentOffset)
case .ended, .cancelled: case .ended, .cancelled:
self.ended?() self.ended?()
default: default:
@ -152,12 +159,14 @@ public final class PinchSourceContainerNode: ASDisplayNode {
} }
} }
public var maxPinchScale: CGFloat = 10.0
private var isActive: Bool = false private var isActive: Bool = false
public var activate: ((PinchSourceContainerNode) -> Void)? public var activate: ((PinchSourceContainerNode) -> Void)?
public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
var deactivate: (() -> Void)? var deactivate: (() -> Void)?
var updated: ((CGFloat, CGPoint) -> Void)? var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
override public init() { override public init() {
self.gesture = PinchSourceGesture() self.gesture = PinchSourceGesture()
@ -187,12 +196,12 @@ public final class PinchSourceContainerNode: ASDisplayNode {
strongSelf.deactivate?() strongSelf.deactivate?()
} }
self.gesture.updated = { [weak self] scale, offset in self.gesture.updated = { [weak self] scale, pinchLocation, offset in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.updated?(scale, offset) strongSelf.updated?(min(scale, strongSelf.maxPinchScale), pinchLocation, offset)
strongSelf.scaleUpdated?(scale, .immediate) strongSelf.scaleUpdated?(min(scale, strongSelf.maxPinchScale), .immediate)
} }
} }
@ -226,7 +235,14 @@ public final class PinchSourceContainerNode: ASDisplayNode {
private final class PinchControllerNode: ViewControllerTracingNode { private final class PinchControllerNode: ViewControllerTracingNode {
private weak var controller: PinchController? private weak var controller: PinchController?
private var initialSourceFrame: CGRect?
private let clippingNode: ASDisplayNode
private let scrollingContainer: ASDisplayNode
private let sourceNode: PinchSourceContainerNode private let sourceNode: PinchSourceContainerNode
private let getContentAreaInScreenSpace: () -> CGRect
private let dimNode: ASDisplayNode private let dimNode: ASDisplayNode
@ -235,17 +251,25 @@ private final class PinchControllerNode: ViewControllerTracingNode {
private var hapticFeedback: HapticFeedback? private var hapticFeedback: HapticFeedback?
init(controller: PinchController, sourceNode: PinchSourceContainerNode) { init(controller: PinchController, sourceNode: PinchSourceContainerNode, getContentAreaInScreenSpace: @escaping () -> CGRect) {
self.controller = controller self.controller = controller
self.sourceNode = sourceNode self.sourceNode = sourceNode
self.getContentAreaInScreenSpace = getContentAreaInScreenSpace
self.dimNode = ASDisplayNode() self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.dimNode.alpha = 0.0 self.dimNode.alpha = 0.0
self.clippingNode = ASDisplayNode()
self.clippingNode.clipsToBounds = true
self.scrollingContainer = ASDisplayNode()
super.init() super.init()
self.addSubnode(self.dimNode) self.addSubnode(self.dimNode)
self.addSubnode(self.clippingNode)
self.clippingNode.addSubnode(self.scrollingContainer)
self.sourceNode.deactivate = { [weak self] in self.sourceNode.deactivate = { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -254,12 +278,22 @@ private final class PinchControllerNode: ViewControllerTracingNode {
strongSelf.controller?.dismiss() strongSelf.controller?.dismiss()
} }
self.sourceNode.updated = { [weak self] scale, offset in self.sourceNode.updated = { [weak self] scale, pinchLocation, offset in
guard let strongSelf = self else { guard let strongSelf = self, let initialSourceFrame = strongSelf.initialSourceFrame else {
return return
} }
strongSelf.dimNode.alpha = max(0.0, min(1.0, scale - 1.0)) 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 self.validLayout = layout
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) 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() { 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.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) { func animateOut(completion: @escaping () -> Void) {
self.isAnimatingOut = true
let performCompletion: () -> Void = { [weak self] in let performCompletion: () -> Void = { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.isAnimatingOut = false
strongSelf.sourceNode.restoreToNaturalSize() strongSelf.sourceNode.restoreToNaturalSize()
strongSelf.sourceNode.addSubnode(strongSelf.sourceNode.contentNode) strongSelf.sourceNode.addSubnode(strongSelf.sourceNode.contentNode)
completion() completion()
} }
if let (scale, offset) = self.sourceNode.gesture.currentTransform { let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view)
let duration = 0.4 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) let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: .spring)
if self.hapticFeedback == nil { if self.hapticFeedback == nil {
self.hapticFeedback = HapticFeedback() self.hapticFeedback = HapticFeedback()
} }
self.hapticFeedback?.prepareImpact(.light) self.hapticFeedback?.prepareImpact(.light)
Queue.mainQueue().after(0.2, { [weak self] in self.hapticFeedback?.impact(.light)
guard let strongSelf = self else {
return
}
strongSelf.hapticFeedback?.impact(.light)
})
self.sourceNode.scaleUpdated?(1.0, transition) 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.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.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() 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) dimNodeTransition.updateAlpha(node: self.dimNode, alpha: 0.0)
} else { } else {
performCompletion() 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 { public final class PinchController: ViewController, StandalonePresentableController {
@ -335,6 +410,7 @@ public final class PinchController: ViewController, StandalonePresentableControl
} }
private let sourceNode: PinchSourceContainerNode private let sourceNode: PinchSourceContainerNode
private let getContentAreaInScreenSpace: () -> CGRect
private var wasDismissed = false private var wasDismissed = false
@ -342,8 +418,9 @@ public final class PinchController: ViewController, StandalonePresentableControl
return self.displayNode as! PinchControllerNode return self.displayNode as! PinchControllerNode
} }
public init(sourceNode: PinchSourceContainerNode) { public init(sourceNode: PinchSourceContainerNode, getContentAreaInScreenSpace: @escaping () -> CGRect) {
self.sourceNode = sourceNode self.sourceNode = sourceNode
self.getContentAreaInScreenSpace = getContentAreaInScreenSpace
super.init(navigationBarPresentationData: nil) super.init(navigationBarPresentationData: nil)
@ -361,7 +438,7 @@ public final class PinchController: ViewController, StandalonePresentableControl
} }
override public func loadDisplayNode() { 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() 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 skipReadHistory(PresentationTheme, Bool)
case crashOnSlowQueries(PresentationTheme, Bool) case crashOnSlowQueries(PresentationTheme, Bool)
case clearTips(PresentationTheme) case clearTips(PresentationTheme)
case reimport(PresentationTheme) case crash(PresentationTheme)
case resetData(PresentationTheme) case resetData(PresentationTheme)
case resetDatabase(PresentationTheme) case resetDatabase(PresentationTheme)
case resetDatabaseAndCache(PresentationTheme) case resetDatabaseAndCache(PresentationTheme)
@ -74,6 +74,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case knockoutWallpaper(PresentationTheme, Bool) case knockoutWallpaper(PresentationTheme, Bool)
case demoVideoChats(Bool) case demoVideoChats(Bool)
case experimentalCompatibility(Bool) case experimentalCompatibility(Bool)
case enableNoiseSuppression(Bool)
case playerEmbedding(Bool) case playerEmbedding(Bool)
case playlistPlayback(Bool) case playlistPlayback(Bool)
case voiceConference case voiceConference
@ -93,7 +94,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.logging.rawValue return DebugControllerSection.logging.rawValue
case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
return DebugControllerSection.experiments.rawValue 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 return DebugControllerSection.experiments.rawValue
case .preferredVideoCodec: case .preferredVideoCodec:
return DebugControllerSection.videoExperiments.rawValue return DebugControllerSection.videoExperiments.rawValue
@ -134,7 +135,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 12 return 12
case .clearTips: case .clearTips:
return 13 return 13
case .reimport: case .crash:
return 14 return 14
case .resetData: case .resetData:
return 15 return 15
@ -158,14 +159,16 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 24 return 24
case .experimentalCompatibility: case .experimentalCompatibility:
return 25 return 25
case .playerEmbedding: case .enableNoiseSuppression:
return 26 return 26
case .playlistPlayback: case .playerEmbedding:
return 27 return 27
case .voiceConference: case .playlistPlayback:
return 28 return 28
case .voiceConference:
return 29
case let .preferredVideoCodec(index, _, _, _): case let .preferredVideoCodec(index, _, _, _):
return 29 + index return 30 + index
case .disableVideoAspectScaling: case .disableVideoAspectScaling:
return 100 return 100
case .enableVoipTcp: case .enableVoipTcp:
@ -553,20 +556,9 @@ private enum DebugControllerEntry: ItemListNodeEntry {
}).start() }).start()
} }
}) })
case let .reimport(theme): case let .crash(theme):
return ItemListActionItem(presentationData: presentationData, title: "Reimport Application Data", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(presentationData: presentationData, title: "Crash", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
let appGroupName = "group.\(Bundle.main.bundleIdentifier!)" preconditionFailure()
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 .resetData(theme): case let .resetData(theme):
return ItemListActionItem(presentationData: presentationData, title: "Reset Data", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { 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() }).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): case let .playerEmbedding(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
@ -822,6 +824,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
if isMainApp { if isMainApp {
entries.append(.clearTips(presentationData.theme)) entries.append(.clearTips(presentationData.theme))
} }
entries.append(.crash(presentationData.theme))
entries.append(.resetData(presentationData.theme)) entries.append(.resetData(presentationData.theme))
entries.append(.resetDatabase(presentationData.theme)) entries.append(.resetDatabase(presentationData.theme))
entries.append(.resetDatabaseAndCache(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(.knockoutWallpaper(presentationData.theme, experimentalSettings.knockoutWallpaper))
entries.append(.demoVideoChats(experimentalSettings.demoVideoChats)) entries.append(.demoVideoChats(experimentalSettings.demoVideoChats))
entries.append(.experimentalCompatibility(experimentalSettings.experimentalCompatibility)) entries.append(.experimentalCompatibility(experimentalSettings.experimentalCompatibility))
entries.append(.enableNoiseSuppression(experimentalSettings.enableNoiseSuppression))
entries.append(.playerEmbedding(experimentalSettings.playerEmbedding)) entries.append(.playerEmbedding(experimentalSettings.playerEmbedding))
entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) 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 { guard let strongSelf = self, let controller = strongSelf.controller else {
return 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) controller.window?.presentInGlobalOverlay(pinchController)
}, openPeer: { [weak self] peerId in }, openPeer: { [weak self] peerId in
self?.openPeer(peerId) self?.openPeer(peerId)

View File

@ -235,9 +235,7 @@ public final class SqliteValueBox: ValueBox {
let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil) let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil)
let path = basePath + "/db_sqlite" let path = basePath + "/db_sqlite"
#if DEBUG postboxLog("Instance \(self) opening sqlite at \(path)")
print("Instance \(self) opening sqlite at \(path)")
#endif
#if DEBUG #if DEBUG
let exists = FileManager.default.fileExists(atPath: path) let exists = FileManager.default.fileExists(atPath: path)
@ -298,7 +296,9 @@ public final class SqliteValueBox: ValueBox {
preconditionFailure("Couldn't open database") 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 var resultCode: Bool = true
@ -307,7 +307,11 @@ public final class SqliteValueBox: ValueBox {
resultCode = database.execute("PRAGMA cipher_default_plaintext_header_size=32") resultCode = database.execute("PRAGMA cipher_default_plaintext_header_size=32")
assert(resultCode) assert(resultCode)
postboxLog("Did set up cipher")
if self.isEncrypted(database) { if self.isEncrypted(database) {
postboxLog("Database is encrypted")
if let encryptionParameters = encryptionParameters { if let encryptionParameters = encryptionParameters {
precondition(encryptionParameters.salt.data.count == 16) precondition(encryptionParameters.salt.data.count == 16)
precondition(encryptionParameters.key.data.count == 32) precondition(encryptionParameters.key.data.count == 32)
@ -317,11 +321,14 @@ public final class SqliteValueBox: ValueBox {
resultCode = database.execute("PRAGMA key=\"x'\(hexKey)'\"") resultCode = database.execute("PRAGMA key=\"x'\(hexKey)'\"")
assert(resultCode) assert(resultCode)
postboxLog("Setting encryption key")
if self.isEncrypted(database) { if self.isEncrypted(database) {
postboxLog("Encryption key is invalid")
if isTemporary || isReadOnly { if isTemporary || isReadOnly {
return nil return nil
} }
postboxLog("Encryption key is invalid")
for fileName in dabaseFileNames { for fileName in dabaseFileNames {
let _ = try? FileManager.default.removeItem(atPath: basePath + "/\(fileName)") let _ = try? FileManager.default.removeItem(atPath: basePath + "/\(fileName)")
@ -354,6 +361,8 @@ public final class SqliteValueBox: ValueBox {
assert(resultCode) assert(resultCode)
} }
} else if let encryptionParameters = encryptionParameters, encryptionParameters.forceEncryptionIfNoSet { } else if let encryptionParameters = encryptionParameters, encryptionParameters.forceEncryptionIfNoSet {
postboxLog("Not encrypted")
let hexKey = hexString(encryptionParameters.key.data + encryptionParameters.salt.data) let hexKey = hexString(encryptionParameters.key.data + encryptionParameters.salt.data)
if FileManager.default.fileExists(atPath: path) { if FileManager.default.fileExists(atPath: path) {
@ -410,6 +419,8 @@ public final class SqliteValueBox: ValueBox {
} }
} }
postboxLog("Did set up encryption")
//database.execute("PRAGMA cache_size=-2097152") //database.execute("PRAGMA cache_size=-2097152")
resultCode = database.execute("PRAGMA mmap_size=0") resultCode = database.execute("PRAGMA mmap_size=0")
assert(resultCode) assert(resultCode)
@ -421,6 +432,9 @@ public final class SqliteValueBox: ValueBox {
assert(resultCode) assert(resultCode)
resultCode = database.execute("PRAGMA cipher_memory_security = OFF") resultCode = database.execute("PRAGMA cipher_memory_security = OFF")
assert(resultCode) assert(resultCode)
postboxLog("Did set up pragmas")
//resultCode = database.execute("PRAGMA wal_autocheckpoint=500") //resultCode = database.execute("PRAGMA wal_autocheckpoint=500")
//database.execute("PRAGMA journal_size_limit=1536") //database.execute("PRAGMA journal_size_limit=1536")
@ -442,8 +456,12 @@ public final class SqliteValueBox: ValueBox {
let _ = self.runPragma(database, "checkpoint_fullfsync = 1") let _ = self.runPragma(database, "checkpoint_fullfsync = 1")
assert(self.runPragma(database, "checkpoint_fullfsync") == "1") assert(self.runPragma(database, "checkpoint_fullfsync") == "1")
postboxLog("Did set up checkpoint_fullfsync")
self.beginInternal(database: database) self.beginInternal(database: database)
postboxLog("Did begin transaction")
let result = self.getUserVersion(database) let result = self.getUserVersion(database)
if result < 3 { if result < 3 {
@ -463,8 +481,12 @@ public final class SqliteValueBox: ValueBox {
self.fullTextTables[table.id] = table self.fullTextTables[table.id] = table
} }
postboxLog("Did load tables")
self.commitInternal(database: database) self.commitInternal(database: database)
postboxLog("Did commit final")
lock.unlock() lock.unlock()
return database return database
@ -518,7 +540,21 @@ public final class SqliteValueBox: ValueBox {
private func isEncrypted(_ database: Database) -> Bool { private func isEncrypted(_ database: Database) -> Bool {
var statement: OpaquePointer? = nil 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 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 { if statement == nil {
postboxLog("isEncrypted: sqlite3_prepare_v2 status = \(status) [\(self.databasePath)]") postboxLog("isEncrypted: sqlite3_prepare_v2 status = \(status) [\(self.databasePath)]")
return true return true
@ -536,6 +572,7 @@ public final class SqliteValueBox: ValueBox {
preparedStatement.destroy() preparedStatement.destroy()
return true return true
} }
postboxLog("isEncrypted step done")
preparedStatement.destroy() preparedStatement.destroy()
return status == SQLITE_NOTADB return status == SQLITE_NOTADB
} }

View File

@ -347,7 +347,7 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
var title: String = "" var title: String = ""
var speakerSubtitle: 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 let textColor = UIColor.white
var segments: [AnimatedCountLabelNode.Segment] = [] var segments: [AnimatedCountLabelNode.Segment] = []
var displaySpeakerSubtitle = false var displaySpeakerSubtitle = false
@ -381,7 +381,22 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
} }
displaySpeakerSubtitle = speakerSubtitle != title && !speakerSubtitle.isEmpty 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) var membersPart = presentationData.strings.VoiceChat_Status_Members(membersCount)
if membersPart.contains("[") && membersPart.contains("]") { if membersPart.contains("[") && membersPart.contains("]") {
if let startIndex = membersPart.firstIndex(of: "["), let endIndex = membersPart.firstIndex(of: "]") { if let startIndex = membersPart.firstIndex(of: "["), let endIndex = membersPart.firstIndex(of: "]") {
@ -433,6 +448,19 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
} }
self.backgroundNode.connectingColor = color 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 { if self.subtitleNode.segments != segments && !displaySpeakerSubtitle {

View File

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

View File

@ -557,6 +557,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private var peerUpdatesSubscription: Disposable? private var peerUpdatesSubscription: Disposable?
public private(set) var schedulePending = false public private(set) var schedulePending = false
private var isScheduled = false
private var isScheduledStarted = false
init( init(
accountContext: AccountContext, accountContext: AccountContext,
@ -581,6 +583,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.invite = invite self.invite = invite
self.joinAsPeerId = joinAsPeerId ?? accountContext.account.peerId self.joinAsPeerId = joinAsPeerId ?? accountContext.account.peerId
self.schedulePending = initialCall == nil 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.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title, scheduleTimestamp: initialCall?.scheduleTimestamp, subscribedToScheduled: initialCall?.subscribedToScheduled ?? false)
self.statePromise = ValuePromise(self.stateValue) self.statePromise = ValuePromise(self.stateValue)
@ -1058,6 +1061,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
case let .active(previousCallInfo): case let .active(previousCallInfo):
if case let .active(callInfo) = internalState { if case let .active(callInfo) = internalState {
shouldJoin = previousCallInfo.scheduleTimestamp != nil && callInfo.scheduleTimestamp == nil shouldJoin = previousCallInfo.scheduleTimestamp != nil && callInfo.scheduleTimestamp == nil
self.participantsContext = nil
activeCallInfo = callInfo activeCallInfo = callInfo
} else { } else {
activeCallInfo = nil activeCallInfo = nil
@ -1082,6 +1086,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
outgoingAudioBitrateKbit = value outgoingAudioBitrateKbit = value
} }
let enableNoiseSuppression = accountContext.sharedContext.immediateExperimentalUISettings.enableNoiseSuppression
callContext = OngoingGroupCallContext(video: self.videoCapturer, participantDescriptionsRequired: { [weak self] ssrcs in callContext = OngoingGroupCallContext(video: self.videoCapturer, participantDescriptionsRequired: { [weak self] ssrcs in
Queue.mainQueue().async { Queue.mainQueue().async {
guard let strongSelf = self else { guard let strongSelf = self else {
@ -1098,7 +1104,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
strongSelf.requestCall(movingFromBroadcastToRtc: false) strongSelf.requestCall(movingFromBroadcastToRtc: false)
} }
} }
}, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: self.isVideo) }, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: self.isVideo, enableNoiseSuppression: enableNoiseSuppression)
self.incomingVideoSourcePromise.set(callContext.videoSources self.incomingVideoSourcePromise.set(callContext.videoSources
|> deliverOnMainQueue |> deliverOnMainQueue
|> map { [weak self] sources -> [PeerId: UInt32] in |> map { [weak self] sources -> [PeerId: UInt32] in
@ -1284,10 +1290,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
if !strongSelf.didConnectOnce { if !strongSelf.didConnectOnce {
strongSelf.didConnectOnce = true strongSelf.didConnectOnce = true
if !strongSelf.isScheduled {
let toneRenderer = PresentationCallToneRenderer(tone: .groupJoined) let toneRenderer = PresentationCallToneRenderer(tone: .groupJoined)
strongSelf.toneRenderer = toneRenderer strongSelf.toneRenderer = toneRenderer
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive) toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
} }
}
if let peer = strongSelf.reconnectingAsPeer { if let peer = strongSelf.reconnectingAsPeer {
strongSelf.reconnectingAsPeer = nil strongSelf.reconnectingAsPeer = nil
@ -1711,6 +1719,180 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
if let isCurrentlyConnecting = self.isCurrentlyConnecting, isCurrentlyConnecting { if let isCurrentlyConnecting = self.isCurrentlyConnecting, isCurrentlyConnecting {
self.startCheckingCallIfNeeded() 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,18 +2068,22 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
return transaction.getPeer(peerId) return transaction.getPeer(peerId)
} }
|> deliverOnMainQueue).start(next: { [weak self] myPeer in |> deliverOnMainQueue).start(next: { [weak self] myPeer in
guard let strongSelf = self, let _ = myPeer else { guard let strongSelf = self, let myPeer = myPeer else {
return return
} }
strongSelf.reconnectingAsPeer = myPeer
let previousPeerId = strongSelf.joinAsPeerId let previousPeerId = strongSelf.joinAsPeerId
if let localSsrc = strongSelf.currentLocalSsrc { if let localSsrc = strongSelf.currentLocalSsrc {
strongSelf.ignorePreviousJoinAsPeerId = (previousPeerId, localSsrc) strongSelf.ignorePreviousJoinAsPeerId = (previousPeerId, localSsrc)
} }
strongSelf.joinAsPeerId = peerId strongSelf.joinAsPeerId = peerId
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 { if let participantsContext = strongSelf.participantsContext, let immediateState = participantsContext.immediateState {
for participant in immediateState.participants { for participant in immediateState.participants {
if participant.peer.id == previousPeerId { if participant.peer.id == previousPeerId {
@ -1915,6 +2101,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} }
strongSelf.requestCall(movingFromBroadcastToRtc: false) strongSelf.requestCall(movingFromBroadcastToRtc: false)
}
}) })
} }
@ -2011,20 +2198,32 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.schedulePending = false self.schedulePending = false
self.stateValue.scheduleTimestamp = timestamp 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) self.startDisposable.set((createGroupCall(account: self.account, peerId: self.peerId, title: nil, scheduleDate: timestamp)
|> deliverOnMainQueue).start(next: { [weak self] callInfo in |> deliverOnMainQueue).start(next: { [weak self] callInfo in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.updateSessionState(internalState: .active(callInfo), audioSessionControl: strongSelf.audioSessionControl) strongSelf.updateSessionState(internalState: .active(callInfo), audioSessionControl: strongSelf.audioSessionControl)
}, error: { [weak self] error in
if let strongSelf = self {
strongSelf.markAsCanBeRemoved()
}
})) }))
} }
public func startScheduled() { public func startScheduled() {
guard case let .active(callInfo) = self.internalState else { guard case let .active(callInfo) = self.internalState else {
return return
} }
self.isScheduledStarted = true
self.stateValue.scheduleTimestamp = nil self.stateValue.scheduleTimestamp = nil
self.startDisposable.set((startScheduledGroupCall(account: self.account, peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash) 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 return
} }
strongSelf.updateSessionState(internalState: .active(callInfo), audioSessionControl: strongSelf.audioSessionControl) 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 account = self.account
let currentCall: Signal<GroupCallInfo?, CallError> let currentCall: Signal<GroupCallInfo?, CallError>
if let initialCall = self.initialCall { if let initialCall = self.initialCall {
currentCall = getCurrentGroupCall(account: account, callId: initialCall.id, accessHash: initialCall.accessHash) currentCall = getCurrentGroupCall(account: account, callId: initialCall.id, accessHash: initialCall.accessHash)
@ -2254,6 +2456,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|> map { summary -> GroupCallInfo? in |> map { summary -> GroupCallInfo? in
return summary?.info 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 { } else {
currentCall = .single(nil) currentCall = .single(nil)
} }
@ -2319,7 +2529,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
guard let callInfo = self.internalState.callInfo else { guard let callInfo = self.internalState.callInfo else {
return 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() 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 subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
let totalHeight = titleSize.height + subtitleSize.height + 1.0 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.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) self.bottomNode.frame = CGRect(origin: CGPoint(), size: size)
@ -361,6 +361,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
} }
var backgroundState: VoiceChatActionButtonBackgroundNode.State var backgroundState: VoiceChatActionButtonBackgroundNode.State
var animated = true
switch state { switch state {
case let .button(text): case let .button(text):
backgroundState = .button 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) 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: case .scheduled:
backgroundState = .disabled backgroundState = .disabled
if previousState == .connecting {
animated = false
}
case let .active(state): case let .active(state):
switch state { switch state {
case .on: case .on:
@ -385,7 +389,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
self.applyIconParams() self.applyIconParams()
self.backgroundNode.isDark = dark 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 { if case .active = state, let previousState = previousState, case .connecting = previousState, animated {
self.activeDisposable.set((self.activePromise.get() self.activeDisposable.set((self.activePromise.get()
@ -755,7 +759,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
case muted case muted
} }
func updateGlowAndGradientAnimations(type: Gradient, previousType: Gradient? = nil) { func updateGlowAndGradientAnimations(type: Gradient, previousType: Gradient? = nil, animated: Bool = true) {
let effectivePreviousTyoe = previousType ?? .active let effectivePreviousTyoe = previousType ?? .active
let scale: CGFloat let scale: CGFloat
@ -794,13 +798,15 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
self.maskGradientLayer.transform = CATransform3DMakeScale(targetScale, targetScale, 1.0) self.maskGradientLayer.transform = CATransform3DMakeScale(targetScale, targetScale, 1.0)
if let _ = previousType { if let _ = previousType {
self.maskGradientLayer.animateScale(from: initialScale, to: targetScale, duration: 0.3) 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.maskGradientLayer.animateSpring(from: initialScale as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", duration: 0.45)
} }
self.foregroundGradientLayer.colors = targetColors self.foregroundGradientLayer.colors = targetColors
if animated {
self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
} }
}
private func playMuteAnimation() { private func playMuteAnimation() {
self.maskBlobView.startAnimating() self.maskBlobView.startAnimating()
@ -1081,6 +1087,16 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
self.playMuteAnimation() self.playMuteAnimation()
} }
self.transition = nil 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: case .button:
self.updatedActive?(true) self.updatedActive?(true)
@ -1537,7 +1553,6 @@ final class VoiceChatActionButtonIconNode: ManagedAnimationNode {
self.isColored = isColored self.isColored = isColored
super.init(size: CGSize(width: 100.0, height: 100.0)) 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)) 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 useTiredAnimation = false
var useAngryAnimation = false
let val = Float.random(in: 0.0..<1.0) let val = Float.random(in: 0.0..<1.0)
if val <= 0.01 { if val <= 0.01 {
useTiredAnimation = true 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 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() { if let animationName = animations.randomElement() {
self.trackTo(item: ManagedAnimationItem(source: .local(animationName))) self.trackTo(item: ManagedAnimationItem(source: .local(animationName)))
} }

View File

@ -830,6 +830,9 @@ public final class VoiceChatController: ViewController {
self.scheduleTextNode = ImmediateTextNode() self.scheduleTextNode = ImmediateTextNode()
self.scheduleTextNode.isHidden = !self.isScheduling 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 = 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 self.scheduleCancelButton.isHidden = !self.isScheduling
@ -840,6 +843,7 @@ public final class VoiceChatController: ViewController {
self.dateFormatter.timeZone = TimeZone.current self.dateFormatter.timeZone = TimeZone.current
self.timerNode = VoiceChatTimerNode(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat) self.timerNode = VoiceChatTimerNode(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat)
self.timerNode.isHidden = true
super.init() 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 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 { if let strongSelf = self, let bio = bio {
if peer.id.namespace == Namespaces.Peer.CloudUser {
let _ = (updateAbout(account: strongSelf.context.account, about: bio) let _ = (updateAbout(account: strongSelf.context.account, about: bio)
|> `catch` { _ -> Signal<Void, NoError> in |> `catch` { _ -> Signal<Void, NoError> in
return .complete() return .complete()
}).start() }).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 }) 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.rightBorderNode)
self.contentContainer.addSubnode(self.bottomPanelNode) self.contentContainer.addSubnode(self.bottomPanelNode)
self.contentContainer.addSubnode(self.timerNode) self.contentContainer.addSubnode(self.timerNode)
self.contentContainer.addSubnode(self.scheduleTextNode)
let invitedPeers: Signal<[Peer], NoError> = self.call.invitedPeers let invitedPeers: Signal<[Peer], NoError> = self.call.invitedPeers
|> mapToSignal { ids -> Signal<[Peer], NoError> in |> mapToSignal { ids -> Signal<[Peer], NoError> in
@ -1700,7 +1712,7 @@ public final class VoiceChatController: ViewController {
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in
if let strongSelf = self { if let strongSelf = self {
strongSelf.currentContentOffset = offset strongSelf.currentContentOffset = offset
if strongSelf.animation == nil && !strongSelf.animatingExpansion { if !strongSelf.animatingExpansion && !strongSelf.animatingInsertion && strongSelf.panGestureArguments == nil {
strongSelf.updateFloatingHeaderOffset(offset: offset, transition: transition) 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) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in }, action: { _, f in
f(.dismissWithoutContent) f(.dismissWithoutContent)
@ -2004,7 +2016,7 @@ public final class VoiceChatController: ViewController {
} }
strongSelf.call.setIsNoiseSuppressionEnabled(!strongSelf.isNoiseSuppressionEnabled) strongSelf.call.setIsNoiseSuppressionEnabled(!strongSelf.isNoiseSuppressionEnabled)
}))) })))*/
if let callState = strongSelf.callState, callState.canManageCall { if let callState = strongSelf.callState, callState.canManageCall {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EndVoiceChat, textColor: .destructive, icon: { theme in 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) self.view.addGestureRecognizer(panRecognizer)
if self.isScheduling { if self.isScheduling {
self.setupPickerView() self.setupSchedulePickerView()
self.updateScheduleButtonTitle() self.updateScheduleButtonTitle()
} }
} }
@ -2218,22 +2230,27 @@ public final class VoiceChatController: ViewController {
let currentDate = Date() let currentDate = Date()
var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: currentDate) var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: currentDate)
components.second = 0 components.second = 0
let minute = (components.minute ?? 0) % 5
let next1MinDate = calendar.date(byAdding: .minute, value: 1, to: calendar.date(from: components)!) let roundedDate = calendar.date(from: components)!
let next5MinDate = calendar.date(byAdding: .minute, value: 5 - minute, to: 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) { if let date = calendar.date(byAdding: .day, value: 365, to: currentDate) {
self.pickerView?.maximumDate = date self.pickerView?.maximumDate = date
} }
if let next1MinDate = next1MinDate, let next5MinDate = next5MinDate { if let next1MinDate = next1MinDate, let nextTwoHourDate = nextTwoHourDate {
self.pickerView?.minimumDate = next1MinDate 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? var currentDate: Date?
if let pickerView = self.pickerView { if let pickerView = self.pickerView {
currentDate = pickerView.date currentDate = pickerView.date
@ -2271,7 +2288,9 @@ public final class VoiceChatController: ViewController {
} }
let calendar = Calendar(identifier: .gregorian) 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 let buttonTitle: String
if calendar.isDateInToday(date) { if calendar.isDateInToday(date) {
buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleToday(time).0 buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleToday(time).0
@ -2282,6 +2301,15 @@ public final class VoiceChatController: ViewController {
} }
self.scheduleButtonTitle = buttonTitle 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 { if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) 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) 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 { if let pickerView = self.pickerView {
self.pickerView = nil
pickerView.alpha = 0.0 pickerView.alpha = 0.0
pickerView.layer.animateScale(from: 1.0, to: 0.25, duration: 0.15, removeOnCompletion: false) 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 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.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.timerNode.animateIn() 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)) 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.listNode.isUserInteractionEnabled = true
self.timerNode.alpha = 0.0 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)) self.updateTitle(transition: .animated(duration: 0.2, curve: .easeInOut))
} }
@ -2602,7 +2638,6 @@ public final class VoiceChatController: ViewController {
self.schedule() self.schedule()
} else if callState.canManageCall { } else if callState.canManageCall {
self.call.startScheduled() self.call.startScheduled()
self.transitionToCall()
} else { } else {
self.call.toggleScheduledSubscription(!callState.subscribedToScheduled) self.call.toggleScheduledSubscription(!callState.subscribedToScheduled)
} }
@ -2678,8 +2713,13 @@ public final class VoiceChatController: ViewController {
let _ = (self.inviteLinksPromise.get() let _ = (self.inviteLinksPromise.get()
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { [weak self] inviteLinks in |> deliverOnMainQueue).start(next: { [weak self] inviteLinks in
guard let strongSelf = self else {
return
}
if let inviteLinks = inviteLinks { 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 return
@ -2790,8 +2830,6 @@ public final class VoiceChatController: ViewController {
} else { } else {
topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) 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 { } else if let currentTopInset = self.topInset {
topInset = self.isExpanded ? 0.0 : currentTopInset topInset = self.isExpanded ? 0.0 : currentTopInset
} else { } else {
@ -2845,7 +2883,7 @@ public final class VoiceChatController: ViewController {
var bottomEdge: CGFloat = 0.0 var bottomEdge: CGFloat = 0.0
self.listNode.forEachItemNode { itemNode in self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListViewItemNode { 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 { if convertedFrame.maxY > bottomEdge {
bottomEdge = convertedFrame.maxY bottomEdge = convertedFrame.maxY
} }
@ -2853,14 +2891,7 @@ public final class VoiceChatController: ViewController {
} }
let listMaxY = listTopInset + listSize.height let listMaxY = listTopInset + listSize.height
if bottomEdge.isZero { let bottomOffset: CGFloat = min(0.0, bottomEdge - listMaxY)
bottomEdge = listMaxY
}
var bottomOffset: CGFloat = 0.0
if bottomEdge < listMaxY && (self.panGestureArguments != nil || self.isExpanded) {
bottomOffset = 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 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 let previousBottomCornersFrame = self.bottomCornersNode.frame
@ -3139,9 +3170,7 @@ public final class VoiceChatController: ViewController {
topInset = listSize.height 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 (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listSize, insets: insets, duration: duration, curve: curve) 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) transition.updateFrame(node: self.timerNode, frame: timerFrame)
self.timerNode.update(size: timerFrame.size, scheduleTime: self.callState?.scheduleTimestamp, transition: .immediate) 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 centralButtonSide = min(size.width, size.height) - 32.0
let centralButtonSize = CGSize(width: centralButtonSide, height: centralButtonSide) let centralButtonSize = CGSize(width: centralButtonSide, height: centralButtonSide)
let cameraButtonSize = CGSize(width: 36.0, height: 36.0) let cameraButtonSize = CGSize(width: 36.0, height: 36.0)
@ -3188,7 +3220,7 @@ public final class VoiceChatController: ViewController {
smallButtons = false smallButtons = false
firstButtonFrame = CGRect(origin: CGPoint(x: floor(leftButtonFrame.midX - cameraButtonSize.width / 2.0), y: leftButtonFrame.minY - upperButtonDistance - cameraButtonSize.height), size: cameraButtonSize) firstButtonFrame = CGRect(origin: CGPoint(x: floor(leftButtonFrame.midX - cameraButtonSize.width / 2.0), y: leftButtonFrame.minY - upperButtonDistance - cameraButtonSize.height), size: cameraButtonSize)
secondButtonFrame = leftButtonFrame 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 forthButtonFrame = rightButtonFrame
case let .fullscreen(controlsHidden): case let .fullscreen(controlsHidden):
smallButtons = true smallButtons = true
@ -3398,10 +3430,13 @@ public final class VoiceChatController: ViewController {
self.enqueuedTransitions.remove(at: 0) self.enqueuedTransitions.remove(at: 0)
if self.callState?.scheduleTimestamp != nil && self.listNode.alpha > 0.0 { if self.callState?.scheduleTimestamp != nil && self.listNode.alpha > 0.0 {
self.timerNode.isHidden = false
self.listNode.alpha = 0.0 self.listNode.alpha = 0.0
self.listNode.isUserInteractionEnabled = false self.listNode.isUserInteractionEnabled = false
self.backgroundNode.backgroundColor = panelBackgroundColor self.backgroundNode.backgroundColor = panelBackgroundColor
self.updateIsFullscreen(false) self.updateIsFullscreen(false)
} else if self.callState?.scheduleTimestamp == nil && !self.isScheduling && self.listNode.alpha == 0.0 {
self.transitionToCall()
} }
var options = ListViewDeleteAndInsertOptions() var options = ListViewDeleteAndInsertOptions()
@ -3412,7 +3447,7 @@ public final class VoiceChatController: ViewController {
if transition.crossFade { if transition.crossFade {
options.insert(.AnimateCrossfade) options.insert(.AnimateCrossfade)
} }
if transition.animated && self.animation == nil { if transition.animated {
options.insert(.AnimateInsertion) options.insert(.AnimateInsertion)
} }
} }
@ -3443,11 +3478,7 @@ public final class VoiceChatController: ViewController {
let listTopInset = layoutTopInset + 63.0 let listTopInset = layoutTopInset + 63.0
let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight) 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) 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)))
}
let targetY = listTopInset + (self.topInset ?? listSize.height) let targetY = listTopInset + (self.topInset ?? listSize.height)
@ -3455,24 +3486,20 @@ public final class VoiceChatController: ViewController {
var frame = self.listNode.frame var frame = self.listNode.frame
frame.origin.y = targetY frame.origin.y = targetY
self.listNode.frame = frame 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 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 { guard let strongSelf = self else {
return return
} }
if strongSelf.animatingInsertion {
strongSelf.updateFloatingHeaderOffset(offset: self?.currentContentOffset ?? 0.0, transition: .animated(duration: 0.2, curve: .easeInOut))
strongSelf.animatingInsertion = false
}
if !strongSelf.didSetContentsReady { if !strongSelf.didSetContentsReady {
strongSelf.didSetContentsReady = true strongSelf.didSetContentsReady = true
strongSelf.controller?.contentsReady.set(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>) { private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, callMembers: ([GroupCallParticipantsContext.Participant], String?), invitedPeers: [Peer], speakingPeers: Set<PeerId>) {
var disableAnimation = false var disableAnimation = false
if self.currentCallMembers?.1 != callMembers.1 { if self.currentCallMembers?.1 != callMembers.1 {
@ -3689,6 +3683,8 @@ public final class VoiceChatController: ViewController {
self.itemInteraction?.isExpanded = self.isExpanded self.itemInteraction?.isExpanded = self.isExpanded
} }
} }
private var animatingInsertion = false
private var animatingExpansion = false private var animatingExpansion = false
private var panGestureArguments: (topInset: CGFloat, offset: CGFloat)? private var panGestureArguments: (topInset: CGFloat, offset: CGFloat)?
@ -3914,8 +3910,9 @@ public final class VoiceChatController: ViewController {
return 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 let initialTitle = strongSelf.callState?.title ?? ""
if let strongSelf = self, let title = 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.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 }) 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) context.setFillColor(parameters.color.cgColor)
var clearLineWidth: CGFloat = 4.0 var clearLineWidth: CGFloat = 2.0
var lineWidth: CGFloat = 1.0 + UIScreenPixel var lineWidth: CGFloat = 1.0 + UIScreenPixel
if bounds.size.width > 36.0 { if bounds.size.width > 36.0 {
context.scaleBy(x: 2.0, y: 2.0) context.scaleBy(x: 2.0, y: 2.0)
} else if bounds.size.width < 30.0 { } else if bounds.size.width < 30.0 {
clearLineWidth = 3.0 clearLineWidth = 2.0
lineWidth = 1.0 lineWidth = 1.0
} }
@ -207,18 +207,19 @@ final class VoiceChatMicrophoneNode: ASDisplayNode {
} }
if parameters.reverse { if parameters.reverse {
startPoint = CGPoint(x: origin.x + length * (1.0 - parameters.transition), y: origin.y + length * (1.0 - parameters.transition)) 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) endPoint = CGPoint(x: origin.x + length, y: origin.y + length).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel)
} else { } else {
startPoint = origin startPoint = origin.offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel)
endPoint = CGPoint(x: origin.x + length * parameters.transition, y: origin.y + length * parameters.transition) endPoint = CGPoint(x: origin.x + length * parameters.transition, y: origin.y + length * parameters.transition).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel)
} }
context.setBlendMode(.clear) context.setBlendMode(.clear)
context.setLineWidth(clearLineWidth) context.setLineWidth(clearLineWidth)
context.move(to: startPoint) context.move(to: startPoint.offsetBy(dx: 0.0, dy: 1.0 + UIScreenPixel))
context.addLine(to: endPoint) context.addLine(to: endPoint.offsetBy(dx: 0.0, dy: 1.0 + UIScreenPixel))
context.strokePath() context.strokePath()
context.setBlendMode(.normal) context.setBlendMode(.normal)

View File

@ -169,7 +169,7 @@ public final class VoiceChatOverlayController: ViewController {
if reclaim { if reclaim {
self.dismissed = true 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 { if self.isSlidOffscreen {
self.isSlidOffscreen = false self.isSlidOffscreen = false
self.isButtonHidden = true self.isButtonHidden = true

View File

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

View File

@ -551,6 +551,10 @@ private final class VoiceChatUserNameEditAlertContentNode: AlertContentNode {
} }
self.updateTheme(theme) self.updateTheme(theme)
self.firstNameInputFieldNode.complete = { [weak self] in
self?.lastNameInputFieldNode.activateInput()
}
} }
deinit { 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 invoice: BotPaymentInvoice
public let info: BotPaymentRequestedInfo? public let info: BotPaymentRequestedInfo?
public let shippingOption: BotPaymentShippingOption? public let shippingOption: BotPaymentShippingOption?
public let credentialsTitle: String public let credentialsTitle: String
public let invoiceMedia: TelegramMediaInvoice
public let tipAmount: Int64? public let tipAmount: Int64?
} }
@ -419,15 +420,56 @@ public func requestBotPaymentReceipt(account: Account, messageId: MessageId) ->
|> mapError { _ -> RequestBotPaymentReceiptError in |> mapError { _ -> RequestBotPaymentReceiptError in
return .generic return .generic
} }
|> map { result -> BotPaymentReceipt in |> mapToSignal { result -> Signal<BotPaymentReceipt, RequestBotPaymentReceiptError> in
return account.postbox.transaction { transaction -> BotPaymentReceipt in
switch result { switch result {
case let .paymentReceipt(flags, date, botId, providerId, title, description, photo, invoice, info, shipping, tipAmount, currency, totalAmount, credentialsTitle, users): 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 parsedInvoice = BotPaymentInvoice(apiInvoice: invoice)
let parsedInfo = info.flatMap(BotPaymentRequestedInfo.init) let parsedInfo = info.flatMap(BotPaymentRequestedInfo.init)
let shippingOption = shipping.flatMap(BotPaymentShippingOption.init) let shippingOption = shipping.flatMap(BotPaymentShippingOption.init)
return BotPaymentReceipt(invoice: parsedInvoice, info: parsedInfo, shippingOption: shippingOption, credentialsTitle: credentialsTitle, tipAmount: tipAmount)
/*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 { public enum CreateGroupCallError {
case generic case generic
case anonymousNotAllowed case anonymousNotAllowed
case scheduledTooLate
} }
public func createGroupCall(account: Account, peerId: PeerId, title: String?, scheduleDate: Int32?) -> Signal<GroupCallInfo, CreateGroupCallError> { 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 |> mapError { error -> CreateGroupCallError in
if error.errorDescription == "ANONYMOUS_CALLS_DISABLED" { if error.errorDescription == "ANONYMOUS_CALLS_DISABLED" {
return .anonymousNotAllowed return .anonymousNotAllowed
} else if error.errorDescription == "SCHEDULE_DATE_TOO_LATE" {
return .scheduledTooLate
} }
return .generic return .generic
} }
@ -1005,6 +1008,11 @@ public final class GroupCallParticipantsContext {
public struct DefaultParticipantsAreMuted: Equatable { public struct DefaultParticipantsAreMuted: Equatable {
public var isMuted: Bool public var isMuted: Bool
public var canChange: Bool public var canChange: Bool
public init(isMuted: Bool, canChange: Bool) {
self.isMuted = isMuted
self.canChange = canChange
}
} }
public var participants: [Participant] 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) }) 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 { private struct OverlayState: Equatable {

View File

@ -359,7 +359,13 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
pageIndicatorInactiveColor: UIColor(white: 1.0, alpha: 0.3), pageIndicatorInactiveColor: UIColor(white: 1.0, alpha: 0.3),
inputClearButtonColor: UIColor(rgb: 0x8b9197), inputClearButtonColor: UIColor(rgb: 0x8b9197),
itemBarChart: PresentationThemeItemBarChart(color1: UIColor(rgb: 0xffffff), color2: UIColor(rgb: 0x929196), color3: UIColor(rgb: 0x333333)), 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( let chatList = PresentationThemeChatList(

View File

@ -612,7 +612,13 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres
pageIndicatorInactiveColor: mainSecondaryTextColor.withAlphaComponent(0.4), pageIndicatorInactiveColor: mainSecondaryTextColor.withAlphaComponent(0.4),
inputClearButtonColor: mainSecondaryColor, inputClearButtonColor: mainSecondaryColor,
itemBarChart: PresentationThemeItemBarChart(color1: accentColor, color2: mainSecondaryTextColor.withAlphaComponent(0.5), color3: accentColor.withMultiplied(hue: 1.038, saturation: 0.329, brightness: 0.33)), 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( let chatList = PresentationThemeChatList(

View File

@ -448,7 +448,13 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
pageIndicatorInactiveColor: UIColor(rgb: 0xe3e3e7), pageIndicatorInactiveColor: UIColor(rgb: 0xe3e3e7),
inputClearButtonColor: UIColor(rgb: 0xcccccc), inputClearButtonColor: UIColor(rgb: 0xcccccc),
itemBarChart: PresentationThemeItemBarChart(color1: UIColor(rgb: 0x007ee5), color2: UIColor(rgb: 0xc8c7cc), color3: UIColor(rgb: 0xf2f1f7)), 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( let chatList = PresentationThemeChatList(

View File

@ -406,6 +406,25 @@ public final class PresentationInputFieldTheme {
} }
public final class PresentationThemeList { 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 blocksBackgroundColor: UIColor
public let plainBackgroundColor: UIColor public let plainBackgroundColor: UIColor
public let itemPrimaryTextColor: UIColor public let itemPrimaryTextColor: UIColor
@ -437,8 +456,42 @@ public final class PresentationThemeList {
public let inputClearButtonColor: UIColor public let inputClearButtonColor: UIColor
public let itemBarChart: PresentationThemeItemBarChart public let itemBarChart: PresentationThemeItemBarChart
public let itemInputField: PresentationInputFieldTheme 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.blocksBackgroundColor = blocksBackgroundColor
self.plainBackgroundColor = plainBackgroundColor self.plainBackgroundColor = plainBackgroundColor
self.itemPrimaryTextColor = itemPrimaryTextColor self.itemPrimaryTextColor = itemPrimaryTextColor
@ -470,10 +523,11 @@ public final class PresentationThemeList {
self.inputClearButtonColor = inputClearButtonColor self.inputClearButtonColor = inputClearButtonColor
self.itemBarChart = itemBarChart self.itemBarChart = itemBarChart
self.itemInputField = itemInputField 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 { 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) 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 { extension PresentationThemeList: Codable {
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case blocksBg case blocksBg
@ -778,6 +805,7 @@ extension PresentationThemeList: Codable {
case inputClearButton case inputClearButton
case itemBarChart case itemBarChart
case itemInputField case itemInputField
case paymentOption
} }
public convenience init(from decoder: Decoder) throws { public convenience init(from decoder: Decoder) throws {
@ -790,6 +818,8 @@ extension PresentationThemeList: Codable {
freePlainInputField = try values.decode(PresentationInputFieldTheme.self, forKey: .freeInputField) freePlainInputField = try values.decode(PresentationInputFieldTheme.self, forKey: .freeInputField)
} }
let freeTextSuccessColor = try decodeColor(values, .freeTextSuccess)
self.init( self.init(
blocksBackgroundColor: try decodeColor(values, .blocksBg), blocksBackgroundColor: try decodeColor(values, .blocksBg),
plainBackgroundColor: try decodeColor(values, .plainBg), plainBackgroundColor: try decodeColor(values, .plainBg),
@ -808,7 +838,7 @@ extension PresentationThemeList: Codable {
sectionHeaderTextColor: try decodeColor(values, .sectionHeaderText), sectionHeaderTextColor: try decodeColor(values, .sectionHeaderText),
freeTextColor: try decodeColor(values, .freeText), freeTextColor: try decodeColor(values, .freeText),
freeTextErrorColor: try decodeColor(values, .freeTextError), freeTextErrorColor: try decodeColor(values, .freeTextError),
freeTextSuccessColor: try decodeColor(values, .freeTextSuccess), freeTextSuccessColor: freeTextSuccessColor,
freeMonoIconColor: try decodeColor(values, .freeMonoIcon), freeMonoIconColor: try decodeColor(values, .freeMonoIcon),
itemSwitchColors: try values.decode(PresentationThemeSwitch.self, forKey: .switch), itemSwitchColors: try values.decode(PresentationThemeSwitch.self, forKey: .switch),
itemDisclosureActions: try values.decode(PresentationThemeItemDisclosureActions.self, forKey: .disclosureActions), itemDisclosureActions: try values.decode(PresentationThemeItemDisclosureActions.self, forKey: .disclosureActions),
@ -821,7 +851,13 @@ extension PresentationThemeList: Codable {
pageIndicatorInactiveColor: try decodeColor(values, .pageIndicatorInactive), pageIndicatorInactiveColor: try decodeColor(values, .pageIndicatorInactive),
inputClearButtonColor: try decodeColor(values, .inputClearButton), inputClearButtonColor: try decodeColor(values, .inputClearButton),
itemBarChart: try values.decode(PresentationThemeItemBarChart.self, forKey: .itemBarChart), 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"] { if let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] {
var result = "" var result = ""
if amount < 0 { if amount < 0 {
@ -198,8 +198,8 @@ public func formatCurrencyAmountCustom(_ amount: Int64, currency: String) -> (St
result.append(entry.symbol) result.append(entry.symbol)
}*/ }*/
return (result, entry.symbol) return (result, entry.symbol, entry.symbolOnLeft)
} else { } else {
return ("", "") return ("", "", false)
} }
} }

View File

@ -449,8 +449,14 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
case let .groupPhoneCall(_, _, scheduleDate, duration): case let .groupPhoneCall(_, _, scheduleDate, duration):
if let scheduleDate = scheduleDate { if let scheduleDate = scheduleDate {
let timeString = humanReadableStringForTimestamp(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: scheduleDate) let timeString = humanReadableStringForTimestamp(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: scheduleDate)
let titleString = strings.Notification_VoiceChatScheduled(timeString).0 if message.author?.id.namespace == Namespaces.Peer.CloudChannel {
let titleString = strings.Notification_VoiceChatScheduledChannel(timeString).0
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor) 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 { } else if let duration = duration {
let titleString = strings.Notification_VoiceChatEnded(callDurationString(strings: strings, value: duration)).0 let titleString = strings.Notification_VoiceChatEnded(callDurationString(strings: strings, value: duration)).0
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor) 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 self.mainWindow.debugAction = nil
let presentationData = sharedContext.currentPresentationData.with { $0 } let presentationData = sharedContext.currentPresentationData.with { $0 }
let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme)) let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme))
navigationController.viewControllers = [debugController(sharedContext: sharedContext, context: nil)] navigationController.viewControllers = [debugController(sharedContext: sharedContext, context: nil)]
self.mainWindow.present(navigationController, on: .root) self.mainWindow.present(navigationController, on: .root)
} }*/
presentationDataPromise.set(sharedContext.presentationData) presentationDataPromise.set(sharedContext.presentationData)

View File

@ -357,6 +357,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private weak var sendMessageActionsController: ChatSendMessageActionSheetController? private weak var sendMessageActionsController: ChatSendMessageActionSheetController?
private var searchResultsController: ChatSearchResultsController? private var searchResultsController: ChatSearchResultsController?
private weak var currentPinchController: PinchController?
private weak var currentPinchSourceItemNode: ListViewItemNode?
private var screenCaptureManager: ScreenCaptureDetectionManager? private var screenCaptureManager: ScreenCaptureDetectionManager?
private let chatAdditionalDataDisposable = MetaDisposable() private let chatAdditionalDataDisposable = MetaDisposable()
@ -579,7 +582,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let text: String let text: String
switch error { switch error {
case .generic: case .generic, .scheduledTooLate:
text = strongSelf.presentationData.strings.Login_UnknownError text = strongSelf.presentationData.strings.Login_UnknownError
case .anonymousNotAllowed: case .anonymousNotAllowed:
text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText
@ -624,12 +627,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.presentAutoremoveSetup() strongSelf.presentAutoremoveSetup()
} }
case .paymentSent: 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 { 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 break
} }
} }*/
return true return true
default: default:
break break
@ -920,7 +924,26 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self else { guard let strongSelf = self else {
return 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) strongSelf.window?.presentInGlobalOverlay(pinchController)
}, openMessageContextActions: { message, node, rect, gesture in }, openMessageContextActions: { message, node, rect, gesture in
gesture?.cancel() gesture?.cancel()
@ -1848,7 +1871,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let invoice = media as? TelegramMediaInvoice { if let invoice = media as? TelegramMediaInvoice {
strongSelf.chatDisplayNode.dismissInput() strongSelf.chatDisplayNode.dismissInput()
if let receiptMessageId = invoice.receiptMessageId { 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 { } else {
strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, messageId: messageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) 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 { guard let strongSelf = self else {
return 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 offset > 0.0 {
if var scrolledToMessageIdValue = strongSelf.scrolledToMessageIdValue { if var scrolledToMessageIdValue = strongSelf.scrolledToMessageIdValue {
@ -4035,6 +4043,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else if offset < 0.0 { } else if offset < 0.0 {
strongSelf.scrolledToMessageIdValue = nil 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 { if case .pinnedMessages = self.presentationInterfaceState.subject {

View File

@ -143,7 +143,6 @@ class ChatMessageShareButton: HighlightableButtonNode {
class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private let contextSourceNode: ContextExtractedContentContainingNode private let contextSourceNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode private let containerNode: ContextControllerSourceNode
private let pinchContainerNode: PinchSourceContainerNode
let imageNode: TransformImageNode let imageNode: TransformImageNode
private var placeholderNode: StickerShimmerEffectNode private var placeholderNode: StickerShimmerEffectNode
private var animationNode: GenericAnimatedStickerNode? private var animationNode: GenericAnimatedStickerNode?
@ -196,7 +195,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
required init() { required init() {
self.contextSourceNode = ContextExtractedContentContainingNode() self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode() self.containerNode = ContextControllerSourceNode()
self.pinchContainerNode = PinchSourceContainerNode()
self.imageNode = TransformImageNode() self.imageNode = TransformImageNode()
self.dateAndStatusNode = ChatMessageDateAndStatusNode() self.dateAndStatusNode = ChatMessageDateAndStatusNode()
@ -264,8 +262,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
self.imageNode.displaysAsynchronously = false self.imageNode.displaysAsynchronously = false
self.containerNode.addSubnode(self.contextSourceNode) self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.pinchContainerNode.contentNode.addSubnode(self.containerNode) self.addSubnode(self.containerNode)
self.addSubnode(self.pinchContainerNode)
self.contextSourceNode.contentNode.addSubnode(self.imageNode) self.contextSourceNode.contentNode.addSubnode(self.imageNode)
self.contextSourceNode.contentNode.addSubnode(self.placeholderNode) self.contextSourceNode.contentNode.addSubnode(self.placeholderNode)
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode) self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
@ -281,23 +278,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} }
item.controllerInteraction.openMessageReactions(item.message.id) 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 { deinit {
@ -996,8 +976,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.containerNode.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.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
@ -1085,7 +1063,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
imageApply() imageApply()
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
strongSelf.pinchContainerNode.contentRect = strongSelf.imageNode.frame
strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect
if let updatedShareButtonNode = updatedShareButtonNode { if let updatedShareButtonNode = updatedShareButtonNode {

View File

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

View File

@ -21,7 +21,6 @@ private let inlineBotNameFont = nameFont
class ChatMessageStickerItemNode: ChatMessageItemView { class ChatMessageStickerItemNode: ChatMessageItemView {
private let contextSourceNode: ContextExtractedContentContainingNode private let contextSourceNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode private let containerNode: ContextControllerSourceNode
private let pinchContainerNode: PinchSourceContainerNode
let imageNode: TransformImageNode let imageNode: TransformImageNode
private var placeholderNode: StickerShimmerEffectNode private var placeholderNode: StickerShimmerEffectNode
var textNode: TextNode? var textNode: TextNode?
@ -54,7 +53,6 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
required init() { required init() {
self.contextSourceNode = ContextExtractedContentContainingNode() self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode() self.containerNode = ContextControllerSourceNode()
self.pinchContainerNode = PinchSourceContainerNode()
self.imageNode = TransformImageNode() self.imageNode = TransformImageNode()
self.placeholderNode = StickerShimmerEffectNode() self.placeholderNode = StickerShimmerEffectNode()
self.placeholderNode.isUserInteractionEnabled = false self.placeholderNode.isUserInteractionEnabled = false
@ -121,8 +119,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
self.imageNode.displaysAsynchronously = false self.imageNode.displaysAsynchronously = false
self.containerNode.addSubnode(self.contextSourceNode) self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.pinchContainerNode.contentNode.addSubnode(self.containerNode) self.addSubnode(self.containerNode)
self.addSubnode(self.pinchContainerNode)
self.contextSourceNode.contentNode.addSubnode(self.placeholderNode) self.contextSourceNode.contentNode.addSubnode(self.placeholderNode)
self.contextSourceNode.contentNode.addSubnode(self.imageNode) self.contextSourceNode.contentNode.addSubnode(self.imageNode)
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode) self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
@ -138,23 +135,6 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
} }
item.controllerInteraction.openMessageReactions(item.message.id) 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) { required init?(coder aDecoder: NSCoder) {
@ -675,12 +655,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.containerNode.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.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
strongSelf.pinchContainerNode.contentRect = strongSelf.imageNode.frame
strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect
dateAndStatusApply(false) dateAndStatusApply(false)

View File

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

View File

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

View File

@ -180,7 +180,7 @@ public final class OngoingGroupCallContext {
private var broadcastPartsSource: BroadcastPartSource? 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 self.queue = queue
var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)? var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)?
@ -224,7 +224,7 @@ public final class OngoingGroupCallContext {
}, },
outgoingAudioBitrateKbit: outgoingAudioBitrateKbit ?? 32, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit ?? 32,
enableVideo: enableVideo, enableVideo: enableVideo,
enableNoiseSuppression: true enableNoiseSuppression: enableNoiseSuppression
) )
let queue = self.queue 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 let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: { 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", "bazel": "4.0.0",
"xcode": "12.4" "xcode": "12.4"
} }