mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-04 21:41:45 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
# Conflicts: # submodules/TelegramCore/Sources/BotPaymentForm.swift
This commit is contained in:
commit
adc3f4b44a
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -48,9 +48,11 @@ jobs:
|
||||
|
||||
cd $SOURCE_DIR
|
||||
|
||||
BUILD_NUMBER_OFFSET="$(cat build_number_offset)"
|
||||
|
||||
export APP_VERSION=$(cat versions.json | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["app"]);')
|
||||
export COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
export COMMIT_COUNT="$(($COMMIT_COUNT+2000))"
|
||||
export COMMIT_COUNT="$(($COMMIT_COUNT+$BUILD_NUMBER_OFFSET))"
|
||||
export BUILD_NUMBER="$COMMIT_COUNT"
|
||||
echo "BUILD_NUMBER=$(echo $BUILD_NUMBER)" >> $GITHUB_ENV
|
||||
echo "APP_VERSION=$(echo $APP_VERSION)" >> $GITHUB_ENV
|
||||
|
||||
@ -70,6 +70,7 @@ beta_testflight:
|
||||
stage: build
|
||||
only:
|
||||
- beta
|
||||
- hotfix
|
||||
except:
|
||||
- tags
|
||||
script:
|
||||
@ -87,6 +88,7 @@ deploy_beta_testflight:
|
||||
stage: deploy
|
||||
only:
|
||||
- beta
|
||||
- hotfix
|
||||
except:
|
||||
- tags
|
||||
script:
|
||||
@ -100,6 +102,7 @@ verifysanity_beta_testflight:
|
||||
stage: verifysanity
|
||||
only:
|
||||
- beta
|
||||
- hotfix
|
||||
except:
|
||||
- tags
|
||||
script:
|
||||
@ -118,6 +121,7 @@ verify_beta_testflight:
|
||||
stage: verify
|
||||
only:
|
||||
- beta
|
||||
- hotfix
|
||||
except:
|
||||
- tags
|
||||
script:
|
||||
|
||||
@ -1 +1 @@
|
||||
6098b6ed7c06e42f7bb7226e92744e7951c4c3f89787d702280f907e68a60a15
|
||||
6eb592f57eca2cd3cda976727ba368ed
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Telegram/Telegram-iOS/Resources/VoiceHand_10.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/VoiceHand_10.tgs
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Telegram/Telegram-iOS/Resources/VoiceHand_8.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/VoiceHand_8.tgs
Normal file
Binary file not shown.
BIN
Telegram/Telegram-iOS/Resources/VoiceHand_9.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/VoiceHand_9.tgs
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -6340,11 +6340,15 @@ Sorry for the inconvenience.";
|
||||
"VoiceChat.PinVideo" = "Pin Video";
|
||||
"VoiceChat.UnpinVideo" = "Unpin Video";
|
||||
|
||||
"Notification.VoiceChatScheduled" = "Voice chat scheduled for %@";
|
||||
"Notification.VoiceChatScheduledChannel" = "Voice chat scheduled for %@";
|
||||
"Notification.VoiceChatScheduled" = "%1$@ Voice chat scheduled for %2$@";
|
||||
|
||||
"VoiceChat.StartsIn" = "Starts in";
|
||||
"VoiceChat.LateBy" = "Late by";
|
||||
|
||||
"VoiceChat.StatusStartsIn" = "starts in %@";
|
||||
"VoiceChat.StatusLateBy" = "late by %@";
|
||||
|
||||
"VoiceChat.StartNow" = "Start Now";
|
||||
"VoiceChat.SetReminder" = "Set Reminder";
|
||||
"VoiceChat.CancelReminder" = "Cancel Reminder";
|
||||
|
||||
1
build_number_offset
Normal file
1
build_number_offset
Normal file
@ -0,0 +1 @@
|
||||
2100
|
||||
@ -79,7 +79,8 @@ COMMIT_ID="$(git rev-parse HEAD)"
|
||||
COMMIT_AUTHOR=$(git log -1 --pretty=format:'%an')
|
||||
if [ -z "$2" ]; then
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
COMMIT_COUNT="$(($COMMIT_COUNT+2000))"
|
||||
BUILD_NUMBER_OFFSET="$(cat build_number_offset)"
|
||||
COMMIT_COUNT="$(($COMMIT_COUNT+$BUILD_NUMBER_OFFSET))"
|
||||
BUILD_NUMBER="$COMMIT_COUNT"
|
||||
else
|
||||
BUILD_NUMBER="$2"
|
||||
|
||||
@ -43,7 +43,7 @@ enum BotCheckoutActionButtonState: Equatable {
|
||||
private let titleFont = Font.semibold(17.0)
|
||||
|
||||
final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
static var diameter: CGFloat = 48.0
|
||||
static var height: CGFloat = 52.0
|
||||
|
||||
private var inactiveFillColor: UIColor
|
||||
private var activeFillColor: UIColor
|
||||
@ -63,11 +63,13 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
self.activeFillColor = activeFillColor
|
||||
self.foregroundColor = foregroundColor
|
||||
|
||||
let diameter: CGFloat = 20.0
|
||||
|
||||
self.progressBackgroundNode = ASImageNode()
|
||||
self.progressBackgroundNode.displaysAsynchronously = false
|
||||
self.progressBackgroundNode.displayWithoutProcessing = true
|
||||
self.progressBackgroundNode.isLayerBacked = true
|
||||
self.progressBackgroundNode.image = generateImage(CGSize(width: BotCheckoutActionButton.diameter, height: BotCheckoutActionButton.diameter), rotatedContext: { size, context in
|
||||
self.progressBackgroundNode.image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
let strokeWidth: CGFloat = 2.0
|
||||
context.setFillColor(activeFillColor.cgColor)
|
||||
@ -75,7 +77,7 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
|
||||
context.setFillColor(inactiveFillColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: strokeWidth, y: strokeWidth), size: CGSize(width: size.width - strokeWidth * 2.0, height: size.height - strokeWidth * 2.0)))
|
||||
let cutout: CGFloat = 10.0
|
||||
let cutout: CGFloat = diameter
|
||||
context.fill(CGRect(origin: CGPoint(x: floor((size.width - cutout) / 2.0), y: 0.0), size: CGSize(width: cutout, height: cutout)))
|
||||
})
|
||||
|
||||
@ -83,14 +85,14 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
self.inactiveBackgroundNode.displaysAsynchronously = false
|
||||
self.inactiveBackgroundNode.displayWithoutProcessing = true
|
||||
self.inactiveBackgroundNode.isLayerBacked = true
|
||||
self.inactiveBackgroundNode.image = generateStretchableFilledCircleImage(diameter: BotCheckoutActionButton.diameter, color: self.foregroundColor, strokeColor: activeFillColor, strokeWidth: 2.0)
|
||||
self.inactiveBackgroundNode.image = generateStretchableFilledCircleImage(diameter: diameter, color: self.foregroundColor, strokeColor: activeFillColor, strokeWidth: 2.0)
|
||||
self.inactiveBackgroundNode.alpha = 0.0
|
||||
|
||||
self.activeBackgroundNode = ASImageNode()
|
||||
self.activeBackgroundNode.displaysAsynchronously = false
|
||||
self.activeBackgroundNode.displayWithoutProcessing = true
|
||||
self.activeBackgroundNode.isLayerBacked = true
|
||||
self.activeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: BotCheckoutActionButton.diameter, color: activeFillColor)
|
||||
self.activeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: diameter, color: activeFillColor)
|
||||
|
||||
self.labelNode = TextNode()
|
||||
self.labelNode.displaysAsynchronously = false
|
||||
@ -178,10 +180,21 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
case .applePay:
|
||||
if case .applePay = previousState {
|
||||
|
||||
if self.applePayButton == nil {
|
||||
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
||||
let applePayButton: PKPaymentButton
|
||||
if #available(iOS 14.0, *) {
|
||||
applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black)
|
||||
} else {
|
||||
|
||||
applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black)
|
||||
}
|
||||
applePayButton.addTarget(self, action: #selector(self.applePayButtonPressed), for: .touchUpInside)
|
||||
self.view.addSubview(applePayButton)
|
||||
self.applePayButton = applePayButton
|
||||
}
|
||||
}
|
||||
if let applePayButton = self.applePayButton {
|
||||
applePayButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: validLayout.width, height: BotCheckoutActionButton.height))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -227,14 +240,18 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func applePayButtonPressed() {
|
||||
self.sendActions(forControlEvents: .touchUpInside, with: nil)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = size
|
||||
|
||||
transition.updateFrame(node: self.progressBackgroundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - BotCheckoutActionButton.diameter) / 2.0), y: 0.0), size: CGSize(width: BotCheckoutActionButton.diameter, height: BotCheckoutActionButton.diameter)))
|
||||
transition.updateFrame(node: self.inactiveBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.diameter)))
|
||||
transition.updateFrame(node: self.activeBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.diameter)))
|
||||
transition.updateFrame(node: self.progressBackgroundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - BotCheckoutActionButton.height) / 2.0), y: 0.0), size: CGSize(width: BotCheckoutActionButton.height, height: BotCheckoutActionButton.height)))
|
||||
transition.updateFrame(node: self.inactiveBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.height)))
|
||||
transition.updateFrame(node: self.activeBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.height)))
|
||||
if let applePayButton = self.applePayButton {
|
||||
applePayButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.diameter))
|
||||
applePayButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.height))
|
||||
}
|
||||
|
||||
var labelSize = self.labelNode.bounds.size
|
||||
|
||||
@ -55,7 +55,7 @@ enum BotCheckoutEntry: ItemListNodeEntry {
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .header:
|
||||
return BotCheckoutSection.header.rawValue
|
||||
return BotCheckoutSection.prices.rawValue
|
||||
case .price, .tip:
|
||||
return BotCheckoutSection.prices.rawValue
|
||||
default:
|
||||
@ -286,7 +286,7 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st
|
||||
|
||||
var index = 0
|
||||
for price in paymentForm.invoice.prices {
|
||||
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false, false))
|
||||
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false, index == 0))
|
||||
totalPrice += price.amount
|
||||
index += 1
|
||||
}
|
||||
@ -448,6 +448,8 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
private var currentTipAmount: Int64?
|
||||
private var formRequestDisposable: Disposable?
|
||||
|
||||
private let actionButtonPanelNode: ASDisplayNode
|
||||
private let actionButtonPanelSeparator: ASDisplayNode
|
||||
private let actionButton: BotCheckoutActionButton
|
||||
private let inProgressDimNode: ASDisplayNode
|
||||
|
||||
@ -481,13 +483,20 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
|
||||
let signal: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError> = combineLatest(context.sharedContext.presentationData, self.state.get(), paymentFormAndInfo.get(), context.account.postbox.loadedPeerWithId(messageId.peerId))
|
||||
|> map { presentationData, state, paymentFormAndInfo, botPeer -> (ItemListPresentationData, (ItemListNodeState, Any)) in
|
||||
let nodeState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: botCheckoutControllerEntries(presentationData: presentationData, state: state, invoice: invoice, paymentForm: paymentFormAndInfo?.0, formInfo: paymentFormAndInfo?.1, validatedFormInfo: paymentFormAndInfo?.2, currentShippingOptionId: paymentFormAndInfo?.3, currentPaymentMethod: paymentFormAndInfo?.4, currentTip: paymentFormAndInfo?.5, botPeer: botPeer), style: .plain, focusItemTag: nil, emptyStateItem: nil, animateChanges: false)
|
||||
let nodeState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: botCheckoutControllerEntries(presentationData: presentationData, state: state, invoice: invoice, paymentForm: paymentFormAndInfo?.0, formInfo: paymentFormAndInfo?.1, validatedFormInfo: paymentFormAndInfo?.2, currentShippingOptionId: paymentFormAndInfo?.3, currentPaymentMethod: paymentFormAndInfo?.4, currentTip: paymentFormAndInfo?.5, botPeer: botPeer), style: .blocks, focusItemTag: nil, emptyStateItem: nil, animateChanges: false)
|
||||
|
||||
return (ItemListPresentationData(presentationData), (nodeState, arguments))
|
||||
}
|
||||
|
||||
self.actionButtonPanelNode = ASDisplayNode()
|
||||
self.actionButtonPanelNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor
|
||||
|
||||
self.actionButtonPanelSeparator = ASDisplayNode()
|
||||
self.actionButtonPanelSeparator.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
|
||||
|
||||
self.actionButton = BotCheckoutActionButton(inactiveFillColor: self.presentationData.theme.list.plainBackgroundColor, activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
self.actionButton.setState(.loading)
|
||||
self.actionButton.setState(.active(""))
|
||||
self.actionButtonPanelNode.isHidden = true
|
||||
|
||||
self.inProgressDimNode = ASDisplayNode()
|
||||
self.inProgressDimNode.alpha = 0.0
|
||||
@ -522,6 +531,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo {
|
||||
strongSelf.currentPaymentMethod = method
|
||||
strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount)))
|
||||
strongSelf.updateActionButton()
|
||||
}
|
||||
}
|
||||
|
||||
@ -551,7 +561,75 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
let canSave = paymentForm.canSaveCredentials || paymentForm.passwordMissing
|
||||
let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, additionalFields: additionalFields, publishableKey: publishableKey, completion: { method in
|
||||
let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, provider: .stripe(additionalFields: additionalFields, publishableKey: publishableKey), completion: { method in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
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 {
|
||||
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.isEnabled = false
|
||||
self.addSubnode(self.actionButton)
|
||||
|
||||
self.listNode.supernode?.insertSubnode(self.inProgressDimNode, aboveSubnode: self.listNode)
|
||||
}
|
||||
@ -775,22 +856,37 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
} else {
|
||||
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))
|
||||
} 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) {
|
||||
var updatedInsets = layout.intrinsicInsets
|
||||
updatedInsets.bottom += BotCheckoutActionButton.diameter + 20.0
|
||||
super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition, additionalInsets: additionalInsets)
|
||||
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter))
|
||||
let bottomPanelHorizontalInset: CGFloat = 16.0
|
||||
let bottomPanelVerticalInset: CGFloat = 16.0
|
||||
let bottomPanelHeight = updatedInsets.bottom + bottomPanelVerticalInset * 2.0 + BotCheckoutActionButton.height
|
||||
|
||||
transition.updateFrame(node: self.actionButtonPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelHeight), size: CGSize(width: layout.size.width, height: bottomPanelHeight)))
|
||||
transition.updateFrame(node: self.actionButtonPanelSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
|
||||
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: bottomPanelHorizontalInset, y: bottomPanelVerticalInset), size: CGSize(width: layout.size.width - bottomPanelHorizontalInset * 2.0, height: BotCheckoutActionButton.height))
|
||||
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
|
||||
self.actionButton.updateLayout(size: actionButtonFrame.size, transition: transition)
|
||||
|
||||
updatedInsets.bottom = bottomPanelHeight
|
||||
|
||||
super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition, additionalInsets: additionalInsets)
|
||||
|
||||
transition.updateFrame(node: self.inProgressDimNode, frame: self.listNode.frame)
|
||||
}
|
||||
|
||||
|
||||
@ -80,7 +80,6 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
@ -110,6 +109,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
@ -209,9 +209,9 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
|
||||
}
|
||||
strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: contentInsets.left, y: contentInsets.top), size: imageSize)
|
||||
|
||||
if strongSelf.backgroundNode.supernode != nil {
|
||||
/*if strongSelf.backgroundNode.supernode != nil {
|
||||
strongSelf.backgroundNode.removeFromSupernode()
|
||||
}
|
||||
}*/
|
||||
if strongSelf.topStripeNode.supernode != nil {
|
||||
strongSelf.topStripeNode.removeFromSupernode()
|
||||
}
|
||||
@ -232,6 +232,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
|
||||
|
||||
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))
|
||||
}
|
||||
})
|
||||
|
||||
@ -30,13 +30,17 @@ struct BotCheckoutNativeCardEntryAdditionalFields: OptionSet {
|
||||
}
|
||||
|
||||
final class BotCheckoutNativeCardEntryController: ViewController {
|
||||
enum Provider {
|
||||
case stripe(additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String)
|
||||
case smartglobal(isTesting: Bool, publicToken: String)
|
||||
}
|
||||
|
||||
private var controllerNode: BotCheckoutNativeCardEntryControllerNode {
|
||||
return super.displayNode as! BotCheckoutNativeCardEntryControllerNode
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let additionalFields: BotCheckoutNativeCardEntryAdditionalFields
|
||||
private let publishableKey: String
|
||||
private let provider: Provider
|
||||
private let completion: (BotCheckoutPaymentMethod) -> Void
|
||||
|
||||
private var presentationData: PresentationData
|
||||
@ -46,10 +50,9 @@ final class BotCheckoutNativeCardEntryController: ViewController {
|
||||
private var doneItem: UIBarButtonItem?
|
||||
private var activityItem: UIBarButtonItem?
|
||||
|
||||
public init(context: AccountContext, additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String, completion: @escaping (BotCheckoutPaymentMethod) -> Void) {
|
||||
public init(context: AccountContext, provider: Provider, completion: @escaping (BotCheckoutPaymentMethod) -> Void) {
|
||||
self.context = context
|
||||
self.additionalFields = additionalFields
|
||||
self.publishableKey = publishableKey
|
||||
self.provider = provider
|
||||
self.completion = completion
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
@ -71,7 +74,7 @@ final class BotCheckoutNativeCardEntryController: ViewController {
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = BotCheckoutNativeCardEntryControllerNode(additionalFields: self.additionalFields, publishableKey: self.publishableKey, theme: self.presentationData.theme, strings: self.presentationData.strings, present: { [weak self] c, a in
|
||||
self.displayNode = BotCheckoutNativeCardEntryControllerNode(provider: self.provider, theme: self.presentationData.theme, strings: self.presentationData.strings, present: { [weak self] c, a in
|
||||
self?.present(c, in: .window(.root), with: a)
|
||||
}, dismiss: { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
|
||||
@ -42,7 +42,7 @@ private final class BotCheckoutNativeCardEntryScrollerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, UIScrollViewDelegate {
|
||||
private let publishableKey: String
|
||||
private let provider: BotCheckoutNativeCardEntryController.Provider
|
||||
|
||||
private let present: (ViewController, Any?) -> Void
|
||||
private let dismiss: () -> Void
|
||||
@ -71,8 +71,10 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
|
||||
private var currentCardData: BotPaymentCardInputData?
|
||||
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) {
|
||||
self.publishableKey = publishableKey
|
||||
private var dataTask: URLSessionDataTask?
|
||||
|
||||
init(provider: BotCheckoutNativeCardEntryController.Provider, theme: PresentationTheme, strings: PresentationStrings, present: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void, openCountrySelection: @escaping () -> Void, updateStatus: @escaping (BotCheckoutNativeCardEntryStatus) -> Void, completion: @escaping (BotCheckoutPaymentMethod) -> Void) {
|
||||
self.provider = provider
|
||||
|
||||
self.present = present
|
||||
self.dismiss = dismiss
|
||||
@ -96,6 +98,8 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
|
||||
}
|
||||
itemNodes.append([BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PaymentCard), self.cardItem])
|
||||
|
||||
switch provider {
|
||||
case let .stripe(additionalFields, _):
|
||||
if additionalFields.contains(.cardholderName) {
|
||||
var sectionItems: [BotPaymentItemNode] = []
|
||||
|
||||
@ -138,6 +142,11 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
|
||||
self.countryItem = 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)
|
||||
itemNodes.append([self.saveInfoItem, BotPaymentTextItemNode(text: strings.Checkout_NewCard_SaveInfoHelp)])
|
||||
@ -214,6 +223,7 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
|
||||
|
||||
deinit {
|
||||
self.verifyDisposable.dispose()
|
||||
self.dataTask?.cancel()
|
||||
}
|
||||
|
||||
func updateCountry(_ iso2: String) {
|
||||
@ -233,9 +243,11 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
|
||||
return
|
||||
}
|
||||
|
||||
switch self.provider {
|
||||
case let .stripe(_, publishableKey):
|
||||
let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration
|
||||
configuration.smsAutofillDisabled = true
|
||||
configuration.publishableKey = self.publishableKey
|
||||
configuration.publishableKey = publishableKey
|
||||
configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph"
|
||||
|
||||
let apiClient = STPAPIClient(configuration: configuration)
|
||||
@ -279,6 +291,100 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
|
||||
}))
|
||||
|
||||
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() {
|
||||
|
||||
@ -29,7 +29,7 @@ class BotCheckoutPriceItem: ListViewItem, ItemListItem {
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = BotCheckoutPriceItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), previousItem, nextItem)
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
@ -48,7 +48,7 @@ class BotCheckoutPriceItem: ListViewItem, ItemListItem {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), previousItem, nextItem)
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
@ -69,13 +69,13 @@ private func priceItemInsets(_ neighbors: ItemListNeighbors) -> UIEdgeInsets {
|
||||
var insets = UIEdgeInsets()
|
||||
switch neighbors.top {
|
||||
case .otherSection:
|
||||
insets.top += 8.0
|
||||
insets.top += 24.0
|
||||
case .none, .sameSection:
|
||||
break
|
||||
}
|
||||
switch neighbors.bottom {
|
||||
case .none, .otherSection:
|
||||
insets.bottom += 8.0
|
||||
insets.bottom += 24.0
|
||||
case .sameSection:
|
||||
break
|
||||
}
|
||||
@ -86,9 +86,9 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
|
||||
let titleNode: TextNode
|
||||
let labelNode: TextNode
|
||||
|
||||
let backgroundNode: ASDisplayNode
|
||||
let separatorNode: ASDisplayNode
|
||||
let bottomSeparatorNode: ASDisplayNode
|
||||
let spacerNode: ASDisplayNode
|
||||
|
||||
private var item: BotCheckoutPriceItem?
|
||||
|
||||
@ -99,37 +99,44 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
|
||||
self.labelNode = TextNode()
|
||||
self.labelNode.isUserInteractionEnabled = false
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.bottomSeparatorNode = ASDisplayNode()
|
||||
self.spacerNode = ASDisplayNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.spacerNode)
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.labelNode)
|
||||
self.addSubnode(self.separatorNode)
|
||||
self.addSubnode(self.bottomSeparatorNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: BotCheckoutPriceItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
func asyncLayout() -> (_ item: BotCheckoutPriceItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors, _ previousItem: ListViewItem?, _ nextItem: ListViewItem?) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
|
||||
return { item, params, neighbors in
|
||||
return { item, params, neighbors, previousItem, nextItem in
|
||||
let rightInset: CGFloat = 16.0 + params.rightInset
|
||||
|
||||
let naturalContentHeight: CGFloat = 34.0
|
||||
|
||||
var contentSize = CGSize(width: params.width, height: naturalContentHeight)
|
||||
var insets = priceItemInsets(neighbors)
|
||||
|
||||
if item.hasSeparator {
|
||||
insets.top += 5.0
|
||||
}
|
||||
let naturalContentHeight: CGFloat
|
||||
var verticalOffset: CGFloat = 0.0
|
||||
if item.isFinal {
|
||||
contentSize.height += 34.0
|
||||
naturalContentHeight = 44.0
|
||||
} else {
|
||||
naturalContentHeight = 34.0
|
||||
}
|
||||
if let _ = previousItem as? BotCheckoutHeaderItem {
|
||||
verticalOffset += 8.0
|
||||
}
|
||||
|
||||
var contentSize = CGSize(width: params.width, height: naturalContentHeight + verticalOffset)
|
||||
if let nextItem = nextItem as? BotCheckoutPriceItem {
|
||||
if nextItem.isFinal {
|
||||
contentSize.height += 8.0
|
||||
}
|
||||
}
|
||||
let insets = priceItemInsets(neighbors)
|
||||
|
||||
let textFont: UIFont
|
||||
let textColor: UIColor
|
||||
@ -154,21 +161,15 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
|
||||
let leftInset: CGFloat = 16.0 + params.leftInset
|
||||
|
||||
strongSelf.separatorNode.isHidden = !item.hasSeparator
|
||||
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.separatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: params.width - leftInset, height: UIScreenPixel))
|
||||
|
||||
strongSelf.bottomSeparatorNode.isHidden = !item.isFinal
|
||||
strongSelf.bottomSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: naturalContentHeight + 10.0), size: CGSize(width: params.width, height: UIScreenPixel))
|
||||
strongSelf.bottomSeparatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: CGSize(width: params.width, height: UIScreenPixel))
|
||||
|
||||
strongSelf.spacerNode.isHidden = !item.isFinal
|
||||
strongSelf.spacerNode.backgroundColor = item.theme.list.blocksBackgroundColor
|
||||
strongSelf.spacerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: naturalContentHeight + 10.0 + UIScreenPixel), size: CGSize(width: params.width, height: max(0.0, contentSize.height - naturalContentHeight - UIScreenPixel)))
|
||||
|
||||
var verticalOffset: CGFloat = 0.0
|
||||
if item.hasSeparator {
|
||||
verticalOffset += 5.0
|
||||
}
|
||||
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: contentSize.height))
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset + floor((naturalContentHeight - titleLayout.size.height) / 2.0)), size: titleLayout.size)
|
||||
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: verticalOffset + floor((naturalContentHeight - labelLayout.size.height) / 2.0)), size: labelLayout.size)
|
||||
|
||||
@ -122,7 +122,7 @@ private final class TipValueNode: ASDisplayNode {
|
||||
|
||||
func update(theme: PresentationTheme, text: String, isHighlighted: Bool, height: CGFloat) -> (CGFloat, (CGFloat) -> Void) {
|
||||
var updateBackground = false
|
||||
let backgroundColor = isHighlighted ? UIColor(rgb: 0x00A650) : UIColor(rgb: 0xE5F6ED)
|
||||
let backgroundColor = isHighlighted ? theme.list.paymentOption.activeFillColor : theme.list.paymentOption.inactiveFillColor
|
||||
if let currentBackgroundColor = self.currentBackgroundColor {
|
||||
if !currentBackgroundColor.isEqual(backgroundColor) {
|
||||
updateBackground = true
|
||||
@ -135,7 +135,7 @@ private final class TipValueNode: ASDisplayNode {
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: backgroundColor)
|
||||
}
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: text, font: Font.semibold(15.0), textColor: isHighlighted ? UIColor(rgb: 0xffffff) : UIColor(rgb: 0x00A650))
|
||||
self.titleNode.attributedText = NSAttributedString(string: text, font: Font.semibold(15.0), textColor: isHighlighted ? theme.list.paymentOption.activeForegroundColor : theme.list.paymentOption.inactiveForegroundColor)
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: height))
|
||||
|
||||
let minWidth: CGFloat = 80.0
|
||||
@ -154,20 +154,23 @@ private final class TipValueNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
let titleNode: TextNode
|
||||
let labelNode: TextNode
|
||||
let tipMeasurementNode: ImmediateTextNode
|
||||
let tipCurrencyNode: ImmediateTextNode
|
||||
private let textNode: TextFieldNode
|
||||
|
||||
private var formatterDelegate: CurrencyUITextFieldDelegate?
|
||||
|
||||
private let scrollNode: ASScrollNode
|
||||
private var valueNodes: [TipValueNode] = []
|
||||
|
||||
private var item: BotCheckoutTipItem?
|
||||
|
||||
private var formatterDelegate: CurrencyUITextFieldDelegate?
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
|
||||
@ -192,6 +195,8 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.labelNode)
|
||||
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 currencyText: (String, String) = formatCurrencyAmountCustom(item.numericValue, currency: item.currency)
|
||||
let currencyText: (String, String, Bool) = formatCurrencyAmountCustom(item.numericValue, currency: item.currency)
|
||||
|
||||
let currencySymbolOnTheLeft = currencyText.2
|
||||
//let currencySymbolOnTheLeft = true
|
||||
|
||||
if strongSelf.textNode.textField.text ?? "" != currencyText.0 {
|
||||
strongSelf.textNode.textField.text = currencyText.0
|
||||
strongSelf.labelNode.isHidden = !currencyText.0.isEmpty
|
||||
@ -281,10 +290,16 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
||||
strongSelf.tipMeasurementNode.attributedText = NSAttributedString(string: currencyText.0, font: titleFont, textColor: textColor)
|
||||
let inputTextSize = strongSelf.tipMeasurementNode.updateLayout(textInputFrame.size)
|
||||
|
||||
let spaceRect = NSAttributedString(string: " ", font: titleFont, textColor: textColor).boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
|
||||
|
||||
strongSelf.tipCurrencyNode.attributedText = NSAttributedString(string: "\(currencyText.1)", font: titleFont, textColor: textColor)
|
||||
let currencySize = strongSelf.tipCurrencyNode.updateLayout(CGSize(width: 100.0, height: .greatestFiniteMagnitude))
|
||||
if currencySymbolOnTheLeft {
|
||||
strongSelf.tipCurrencyNode.frame = CGRect(origin: CGPoint(x: textInputFrame.maxX - currencySize.width - inputTextSize.width - spaceRect.width, y: floor((labelsContentHeight - currencySize.height) / 2.0) - 1.0), size: currencySize)
|
||||
} else {
|
||||
strongSelf.tipCurrencyNode.frame = CGRect(origin: CGPoint(x: textInputFrame.maxX - currencySize.width, y: floor((labelsContentHeight - currencySize.height) / 2.0) - 1.0), size: currencySize)
|
||||
textInputFrame.origin.x -= currencySize.width
|
||||
textInputFrame.origin.x -= currencySize.width + spaceRect.width
|
||||
}
|
||||
|
||||
strongSelf.textNode.frame = textInputFrame
|
||||
|
||||
@ -347,6 +362,9 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
||||
|
||||
strongSelf.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: valueY), size: CGSize(width: params.width, height: max(0.0, contentSize.height - valueY)))
|
||||
strongSelf.scrollNode.view.contentSize = CGSize(width: variantsOffset, height: strongSelf.scrollNode.frame.height)
|
||||
|
||||
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: contentSize.height))
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -382,7 +400,7 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
||||
if value > item.maxValue {
|
||||
value = item.maxValue
|
||||
|
||||
let currencyText: (String, String) = formatCurrencyAmountCustom(value, currency: item.currency)
|
||||
let currencyText = formatCurrencyAmountCustom(value, currency: item.currency)
|
||||
if self.textNode.textField.text ?? "" != currencyText.0 {
|
||||
self.textNode.textField.text = currencyText.0
|
||||
}
|
||||
@ -400,6 +418,11 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
||||
}
|
||||
|
||||
@objc public func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
textField.selectedTextRange = textField.textRange(from: textField.endOfDocument, to: textField.endOfDocument)
|
||||
}
|
||||
|
||||
@objc public func textFieldDidChangeSelection(_ textField: UITextField) {
|
||||
textField.selectedTextRange = textField.textRange(from: textField.endOfDocument, to: textField.endOfDocument)
|
||||
}
|
||||
|
||||
@objc public func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
|
||||
@ -117,7 +117,7 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate {
|
||||
|
||||
textInset = max(measuredInset, textInset)
|
||||
|
||||
transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: textInset, y: 3.0), size: CGSize(width: max(1.0, width - textInset - 8.0), height: 40.0)))
|
||||
transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: textInset, y: 0.0), size: CGSize(width: max(1.0, width - textInset - 8.0), height: 40.0)))
|
||||
|
||||
return 44.0
|
||||
}
|
||||
|
||||
@ -20,16 +20,14 @@ public final class BotReceiptController: ViewController {
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let invoice: TelegramMediaInvoice
|
||||
private let messageId: MessageId
|
||||
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private var didPlayPresentationAnimation = false
|
||||
|
||||
public init(context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId) {
|
||||
public init(context: AccountContext, messageId: MessageId) {
|
||||
self.context = context
|
||||
self.invoice = invoice
|
||||
self.messageId = messageId
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
@ -38,10 +36,10 @@ public final class BotReceiptController: ViewController {
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
|
||||
var title = self.presentationData.strings.Checkout_Receipt_Title
|
||||
if invoice.flags.contains(.isTest) {
|
||||
let title = self.presentationData.strings.Checkout_Receipt_Title
|
||||
/*if invoice.flags.contains(.isTest) {
|
||||
title += " (Test)"
|
||||
}
|
||||
}*/
|
||||
self.title = title
|
||||
}
|
||||
|
||||
@ -54,7 +52,7 @@ public final class BotReceiptController: ViewController {
|
||||
if let strongSelf = self {
|
||||
strongSelf.navigationOffset = offset
|
||||
}
|
||||
}, context: self.context, invoice: self.invoice, messageId: self.messageId, dismissAnimated: { [weak self] in
|
||||
}, context: self.context, messageId: self.messageId, dismissAnimated: { [weak self] in
|
||||
self?.dismiss()
|
||||
})
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ private enum BotReceiptSection: Int32 {
|
||||
|
||||
enum BotReceiptEntry: ItemListNodeEntry {
|
||||
case header(PresentationTheme, TelegramMediaInvoice, String)
|
||||
case price(Int, PresentationTheme, String, String, Bool)
|
||||
case price(Int, PresentationTheme, String, String, Bool, Bool)
|
||||
case paymentMethod(PresentationTheme, String, String)
|
||||
case shippingInfo(PresentationTheme, String, String)
|
||||
case shippingMethod(PresentationTheme, String, String)
|
||||
@ -39,7 +39,7 @@ enum BotReceiptEntry: ItemListNodeEntry {
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .header:
|
||||
return BotReceiptSection.header.rawValue
|
||||
return BotReceiptSection.prices.rawValue
|
||||
case .price:
|
||||
return BotReceiptSection.prices.rawValue
|
||||
default:
|
||||
@ -51,7 +51,7 @@ enum BotReceiptEntry: ItemListNodeEntry {
|
||||
switch self {
|
||||
case .header:
|
||||
return 0
|
||||
case let .price(index, _, _, _, _):
|
||||
case let .price(index, _, _, _, _, _):
|
||||
return 1 + Int32(index)
|
||||
case .paymentMethod:
|
||||
return 10000 + 0
|
||||
@ -85,8 +85,8 @@ enum BotReceiptEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsFinal):
|
||||
if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsFinal) = rhs {
|
||||
case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsHasSeparator, lhsFinal):
|
||||
if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsHasSeparator, rhsFinal) = rhs {
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
@ -99,6 +99,9 @@ enum BotReceiptEntry: ItemListNodeEntry {
|
||||
if lhsValue != rhsValue {
|
||||
return false
|
||||
}
|
||||
if lhsHasSeparator != rhsHasSeparator {
|
||||
return false
|
||||
}
|
||||
if lhsFinal != rhsFinal {
|
||||
return false
|
||||
}
|
||||
@ -154,8 +157,8 @@ enum BotReceiptEntry: ItemListNodeEntry {
|
||||
switch self {
|
||||
case let .header(theme, invoice, botName):
|
||||
return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section)
|
||||
case let .price(_, theme, text, value, isFinal):
|
||||
return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: false, sectionId: self.section)
|
||||
case let .price(_, theme, text, value, hasSeparator, isFinal):
|
||||
return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: hasSeparator, sectionId: self.section)
|
||||
case let .paymentMethod(_, text, value):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil)
|
||||
case let .shippingInfo(_, text, value):
|
||||
@ -172,21 +175,23 @@ enum BotReceiptEntry: ItemListNodeEntry {
|
||||
}
|
||||
}
|
||||
|
||||
private func botReceiptControllerEntries(presentationData: PresentationData, invoice: TelegramMediaInvoice, formInvoice: BotPaymentInvoice?, formInfo: BotPaymentRequestedInfo?, shippingOption: BotPaymentShippingOption?, paymentMethodTitle: String?, botPeer: Peer?) -> [BotReceiptEntry] {
|
||||
private func botReceiptControllerEntries(presentationData: PresentationData, invoice: TelegramMediaInvoice?, formInvoice: BotPaymentInvoice?, formInfo: BotPaymentRequestedInfo?, shippingOption: BotPaymentShippingOption?, paymentMethodTitle: String?, botPeer: Peer?, tipAmount: Int64?) -> [BotReceiptEntry] {
|
||||
var entries: [BotReceiptEntry] = []
|
||||
|
||||
var botName = ""
|
||||
if let botPeer = botPeer {
|
||||
botName = botPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
}
|
||||
if let invoice = invoice {
|
||||
entries.append(.header(presentationData.theme, invoice, botName))
|
||||
}
|
||||
|
||||
if let formInvoice = formInvoice {
|
||||
var totalPrice: Int64 = 0
|
||||
|
||||
var index = 0
|
||||
for price in formInvoice.prices {
|
||||
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: formInvoice.currency), false))
|
||||
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: formInvoice.currency), index == 0, false))
|
||||
totalPrice += price.amount
|
||||
index += 1
|
||||
}
|
||||
@ -196,13 +201,20 @@ private func botReceiptControllerEntries(presentationData: PresentationData, inv
|
||||
shippingOptionString = shippingOption.title
|
||||
|
||||
for price in shippingOption.prices {
|
||||
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: formInvoice.currency), false))
|
||||
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: formInvoice.currency), index == 0, false))
|
||||
totalPrice += price.amount
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
entries.append(.paymentMethod(presentationData.theme, presentationData.strings.Checkout_PaymentMethod, paymentMethodTitle))
|
||||
@ -262,12 +274,12 @@ final class BotReceiptControllerNode: ItemListControllerNode {
|
||||
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private let receiptData = Promise<(BotPaymentInvoice, BotPaymentRequestedInfo?, BotPaymentShippingOption?, String?)?>(nil)
|
||||
private let receiptData = Promise<(BotPaymentInvoice, BotPaymentRequestedInfo?, BotPaymentShippingOption?, String?, TelegramMediaInvoice, Int64?)?>(nil)
|
||||
private var dataRequestDisposable: Disposable?
|
||||
|
||||
private let actionButton: BotCheckoutActionButton
|
||||
|
||||
init(controller: ItemListController?, navigationBar: NavigationBar, updateNavigationOffset: @escaping (CGFloat) -> Void, context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId, dismissAnimated: @escaping () -> Void) {
|
||||
init(controller: ItemListController?, navigationBar: NavigationBar, updateNavigationOffset: @escaping (CGFloat) -> Void, context: AccountContext, messageId: MessageId, dismissAnimated: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.dismissAnimated = dismissAnimated
|
||||
|
||||
@ -277,19 +289,19 @@ final class BotReceiptControllerNode: ItemListControllerNode {
|
||||
|
||||
let signal: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError> = combineLatest(context.sharedContext.presentationData, receiptData.get(), context.account.postbox.loadedPeerWithId(messageId.peerId))
|
||||
|> map { presentationData, receiptData, botPeer -> (ItemListPresentationData, (ItemListNodeState, Any)) in
|
||||
let nodeState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: botReceiptControllerEntries(presentationData: presentationData, invoice: invoice, formInvoice: receiptData?.0, formInfo: receiptData?.1, shippingOption: receiptData?.2, paymentMethodTitle: receiptData?.3, botPeer: botPeer), style: .plain, focusItemTag: nil, emptyStateItem: nil, animateChanges: false)
|
||||
let nodeState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: botReceiptControllerEntries(presentationData: presentationData, invoice: receiptData?.4, formInvoice: receiptData?.0, formInfo: receiptData?.1, shippingOption: receiptData?.2, paymentMethodTitle: receiptData?.3, botPeer: botPeer, tipAmount: receiptData?.5), style: .blocks, focusItemTag: nil, emptyStateItem: nil, animateChanges: false)
|
||||
|
||||
return (ItemListPresentationData(presentationData), (nodeState, arguments))
|
||||
}
|
||||
|
||||
self.actionButton = BotCheckoutActionButton(inactiveFillColor: self.presentationData.theme.list.plainBackgroundColor, activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.plainBackgroundColor)
|
||||
self.actionButton.setState(.inactive(self.presentationData.strings.Common_Done))
|
||||
self.actionButton.setState(.active(self.presentationData.strings.Common_Done))
|
||||
|
||||
super.init(controller: controller, navigationBar: navigationBar, updateNavigationOffset: updateNavigationOffset, state: signal)
|
||||
|
||||
self.dataRequestDisposable = (requestBotPaymentReceipt(account: context.account, messageId: messageId) |> deliverOnMainQueue).start(next: { [weak self] receipt in
|
||||
if let strongSelf = self {
|
||||
strongSelf.receiptData.set(.single((receipt.invoice, receipt.info, receipt.shippingOption, receipt.credentialsTitle)))
|
||||
strongSelf.receiptData.set(.single((receipt.invoice, receipt.info, receipt.shippingOption, receipt.credentialsTitle, receipt.invoiceMedia, receipt.tipAmount)))
|
||||
}
|
||||
})
|
||||
|
||||
@ -303,10 +315,11 @@ final class BotReceiptControllerNode: ItemListControllerNode {
|
||||
|
||||
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) {
|
||||
var updatedInsets = layout.intrinsicInsets
|
||||
updatedInsets.bottom += BotCheckoutActionButton.diameter + 20.0
|
||||
updatedInsets.bottom += BotCheckoutActionButton.height + 16.0 * 2.0
|
||||
|
||||
super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition, additionalInsets: additionalInsets)
|
||||
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter))
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: layout.size.height - 16.0 - BotCheckoutActionButton.height - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width - 16.0 * 2.0, height: BotCheckoutActionButton.height))
|
||||
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
|
||||
self.actionButton.updateLayout(size: actionButtonFrame.size, transition: transition)
|
||||
}
|
||||
|
||||
@ -108,6 +108,12 @@ extension CurrencyUITextFieldDelegate: UITextFieldDelegate {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public func textFieldDidChangeSelection(_ textField: UITextField) {
|
||||
if #available(iOSApplicationExtension 13.0, iOS 13.0, *) {
|
||||
passthroughDelegate?.textFieldDidChangeSelection?(textField)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@ -30,15 +30,18 @@ final class PinchSourceGesture: UIPinchGestureRecognizer {
|
||||
|
||||
private let target: Target
|
||||
|
||||
private(set) var currentTransform: (CGFloat, CGPoint)?
|
||||
private(set) var currentTransform: (CGFloat, CGPoint, CGPoint)?
|
||||
|
||||
var began: (() -> Void)?
|
||||
var updated: ((CGFloat, CGPoint) -> Void)?
|
||||
var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
|
||||
var ended: (() -> Void)?
|
||||
|
||||
private var lastLocation: CGPoint?
|
||||
private var initialLocation: CGPoint?
|
||||
private var pinchLocation = CGPoint()
|
||||
private var currentOffset = CGPoint()
|
||||
|
||||
private var currentNumberOfTouches = 0
|
||||
|
||||
init() {
|
||||
self.target = Target()
|
||||
|
||||
@ -52,11 +55,14 @@ final class PinchSourceGesture: UIPinchGestureRecognizer {
|
||||
override func reset() {
|
||||
super.reset()
|
||||
|
||||
self.lastLocation = nil
|
||||
self.currentNumberOfTouches = 0
|
||||
self.initialLocation = nil
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
//self.currentTouches.formUnion(touches)
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
@ -69,40 +75,41 @@ final class PinchSourceGesture: UIPinchGestureRecognizer {
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
if touches.count >= 2 {
|
||||
var locationSum = CGPoint()
|
||||
for touch in touches {
|
||||
let point = touch.location(in: self.view)
|
||||
locationSum.x += point.x
|
||||
locationSum.y += point.y
|
||||
}
|
||||
locationSum.x /= CGFloat(touches.count)
|
||||
locationSum.y /= CGFloat(touches.count)
|
||||
if let lastLocation = self.lastLocation {
|
||||
self.currentOffset = CGPoint(x: locationSum.x - lastLocation.x, y: locationSum.y - lastLocation.y)
|
||||
} else {
|
||||
self.lastLocation = locationSum
|
||||
self.currentOffset = CGPoint()
|
||||
}
|
||||
if let (scale, _) = self.currentTransform {
|
||||
self.currentTransform = (scale, self.currentOffset)
|
||||
self.updated?(scale, self.currentOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func gestureUpdated() {
|
||||
switch self.state {
|
||||
case .began:
|
||||
self.lastLocation = nil
|
||||
self.currentOffset = CGPoint()
|
||||
self.currentTransform = nil
|
||||
|
||||
let pinchLocation = self.location(in: self.view)
|
||||
self.pinchLocation = pinchLocation
|
||||
self.initialLocation = pinchLocation
|
||||
let scale = max(1.0, self.scale)
|
||||
self.currentTransform = (scale, self.pinchLocation, self.currentOffset)
|
||||
|
||||
self.currentNumberOfTouches = self.numberOfTouches
|
||||
|
||||
self.began?()
|
||||
case .changed:
|
||||
let locationSum = self.location(in: self.view)
|
||||
|
||||
if self.numberOfTouches < 2 && self.currentNumberOfTouches >= 2 {
|
||||
self.initialLocation = CGPoint(x: locationSum.x - self.currentOffset.x, y: locationSum.y - self.currentOffset.y)
|
||||
}
|
||||
self.currentNumberOfTouches = self.numberOfTouches
|
||||
|
||||
if let initialLocation = self.initialLocation {
|
||||
self.currentOffset = CGPoint(x: locationSum.x - initialLocation.x, y: locationSum.y - initialLocation.y)
|
||||
}
|
||||
if let (scale, pinchLocation, _) = self.currentTransform {
|
||||
self.currentTransform = (scale, pinchLocation, self.currentOffset)
|
||||
self.updated?(scale, pinchLocation, self.currentOffset)
|
||||
}
|
||||
|
||||
let scale = max(1.0, self.scale)
|
||||
self.currentTransform = (scale, self.currentOffset)
|
||||
self.updated?(scale, self.currentOffset)
|
||||
self.currentTransform = (scale, self.pinchLocation, self.currentOffset)
|
||||
self.updated?(scale, self.pinchLocation, self.currentOffset)
|
||||
case .ended, .cancelled:
|
||||
self.ended?()
|
||||
default:
|
||||
@ -152,12 +159,14 @@ public final class PinchSourceContainerNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public var maxPinchScale: CGFloat = 10.0
|
||||
|
||||
private var isActive: Bool = false
|
||||
|
||||
public var activate: ((PinchSourceContainerNode) -> Void)?
|
||||
public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||
var deactivate: (() -> Void)?
|
||||
var updated: ((CGFloat, CGPoint) -> Void)?
|
||||
var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
|
||||
|
||||
override public init() {
|
||||
self.gesture = PinchSourceGesture()
|
||||
@ -187,12 +196,12 @@ public final class PinchSourceContainerNode: ASDisplayNode {
|
||||
strongSelf.deactivate?()
|
||||
}
|
||||
|
||||
self.gesture.updated = { [weak self] scale, offset in
|
||||
self.gesture.updated = { [weak self] scale, pinchLocation, offset in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updated?(scale, offset)
|
||||
strongSelf.scaleUpdated?(scale, .immediate)
|
||||
strongSelf.updated?(min(scale, strongSelf.maxPinchScale), pinchLocation, offset)
|
||||
strongSelf.scaleUpdated?(min(scale, strongSelf.maxPinchScale), .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,7 +235,14 @@ public final class PinchSourceContainerNode: ASDisplayNode {
|
||||
|
||||
private final class PinchControllerNode: ViewControllerTracingNode {
|
||||
private weak var controller: PinchController?
|
||||
|
||||
private var initialSourceFrame: CGRect?
|
||||
|
||||
private let clippingNode: ASDisplayNode
|
||||
private let scrollingContainer: ASDisplayNode
|
||||
|
||||
private let sourceNode: PinchSourceContainerNode
|
||||
private let getContentAreaInScreenSpace: () -> CGRect
|
||||
|
||||
private let dimNode: ASDisplayNode
|
||||
|
||||
@ -235,17 +251,25 @@ private final class PinchControllerNode: ViewControllerTracingNode {
|
||||
|
||||
private var hapticFeedback: HapticFeedback?
|
||||
|
||||
init(controller: PinchController, sourceNode: PinchSourceContainerNode) {
|
||||
init(controller: PinchController, sourceNode: PinchSourceContainerNode, getContentAreaInScreenSpace: @escaping () -> CGRect) {
|
||||
self.controller = controller
|
||||
self.sourceNode = sourceNode
|
||||
self.getContentAreaInScreenSpace = getContentAreaInScreenSpace
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
self.dimNode.alpha = 0.0
|
||||
|
||||
self.clippingNode = ASDisplayNode()
|
||||
self.clippingNode.clipsToBounds = true
|
||||
|
||||
self.scrollingContainer = ASDisplayNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.dimNode)
|
||||
self.addSubnode(self.clippingNode)
|
||||
self.clippingNode.addSubnode(self.scrollingContainer)
|
||||
|
||||
self.sourceNode.deactivate = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
@ -254,12 +278,22 @@ private final class PinchControllerNode: ViewControllerTracingNode {
|
||||
strongSelf.controller?.dismiss()
|
||||
}
|
||||
|
||||
self.sourceNode.updated = { [weak self] scale, offset in
|
||||
guard let strongSelf = self else {
|
||||
self.sourceNode.updated = { [weak self] scale, pinchLocation, offset in
|
||||
guard let strongSelf = self, let initialSourceFrame = strongSelf.initialSourceFrame else {
|
||||
return
|
||||
}
|
||||
strongSelf.dimNode.alpha = max(0.0, min(1.0, scale - 1.0))
|
||||
strongSelf.sourceNode.contentNode.transform = CATransform3DTranslate(CATransform3DMakeScale(scale, scale, 1.0), offset.x / scale, offset.y / scale, 0.0)
|
||||
|
||||
let pinchOffset = CGPoint(
|
||||
x: pinchLocation.x - initialSourceFrame.width / 2.0,
|
||||
y: pinchLocation.y - initialSourceFrame.height / 2.0
|
||||
)
|
||||
|
||||
var transform = CATransform3DIdentity
|
||||
transform = CATransform3DScale(transform, scale, scale, 0.0)
|
||||
|
||||
strongSelf.sourceNode.contentNode.transform = transform
|
||||
strongSelf.sourceNode.contentNode.position = CGPoint(x: initialSourceFrame.midX + offset.x - pinchOffset.x * (scale - 1.0), y: initialSourceFrame.midY + offset.y - pinchOffset.y * (scale - 1.0))
|
||||
}
|
||||
}
|
||||
|
||||
@ -278,54 +312,95 @@ private final class PinchControllerNode: ViewControllerTracingNode {
|
||||
self.validLayout = layout
|
||||
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
let convertedFrame = convertFrame(self.sourceNode.contentNode.frame, from: self.sourceNode.view, to: self.view)
|
||||
let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view)
|
||||
self.sourceNode.contentNode.frame = convertedFrame
|
||||
self.addSubnode(self.sourceNode.contentNode)
|
||||
self.initialSourceFrame = convertedFrame
|
||||
self.scrollingContainer.addSubnode(self.sourceNode.contentNode)
|
||||
|
||||
var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace()
|
||||
updatedContentAreaInScreenSpace.origin.x = 0.0
|
||||
updatedContentAreaInScreenSpace.size.width = self.bounds.width
|
||||
|
||||
self.clippingNode.layer.animateFrame(from: updatedContentAreaInScreenSpace, to: self.clippingNode.frame, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
self.clippingNode.layer.animateBoundsOriginYAdditive(from: updatedContentAreaInScreenSpace.minY, to: 0.0, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.isAnimatingOut = true
|
||||
|
||||
let performCompletion: () -> Void = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.isAnimatingOut = false
|
||||
|
||||
strongSelf.sourceNode.restoreToNaturalSize()
|
||||
strongSelf.sourceNode.addSubnode(strongSelf.sourceNode.contentNode)
|
||||
|
||||
completion()
|
||||
}
|
||||
|
||||
if let (scale, offset) = self.sourceNode.gesture.currentTransform {
|
||||
let duration = 0.4
|
||||
let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view)
|
||||
self.sourceNode.contentNode.frame = convertedFrame
|
||||
self.initialSourceFrame = convertedFrame
|
||||
|
||||
if let (scale, pinchLocation, offset) = self.sourceNode.gesture.currentTransform, let initialSourceFrame = self.initialSourceFrame {
|
||||
let duration = 0.3
|
||||
let transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut
|
||||
|
||||
var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace()
|
||||
updatedContentAreaInScreenSpace.origin.x = 0.0
|
||||
updatedContentAreaInScreenSpace.size.width = self.bounds.width
|
||||
|
||||
self.clippingNode.layer.animateFrame(from: self.clippingNode.frame, to: updatedContentAreaInScreenSpace, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false)
|
||||
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: .spring)
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.prepareImpact(.light)
|
||||
Queue.mainQueue().after(0.2, { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.hapticFeedback?.impact(.light)
|
||||
})
|
||||
self.hapticFeedback?.impact(.light)
|
||||
|
||||
self.sourceNode.scaleUpdated?(1.0, transition)
|
||||
|
||||
let pinchOffset = CGPoint(
|
||||
x: pinchLocation.x - initialSourceFrame.width / 2.0,
|
||||
y: pinchLocation.y - initialSourceFrame.height / 2.0
|
||||
)
|
||||
|
||||
var transform = CATransform3DIdentity
|
||||
transform = CATransform3DScale(transform, scale, scale, 0.0)
|
||||
|
||||
self.sourceNode.contentNode.transform = CATransform3DIdentity
|
||||
self.sourceNode.contentNode.position = CGPoint(x: initialSourceFrame.midX, y: initialSourceFrame.midY)
|
||||
self.sourceNode.contentNode.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration * 1.2, damping: 110.0)
|
||||
self.sourceNode.contentNode.layer.animatePosition(from: CGPoint(x: offset.x, y: offset.y), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { _ in
|
||||
self.sourceNode.contentNode.layer.animatePosition(from: CGPoint(x: offset.x - pinchOffset.x * (scale - 1.0), y: offset.y - pinchOffset.y * (scale - 1.0)), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { _ in
|
||||
performCompletion()
|
||||
})
|
||||
|
||||
let dimNodeTransition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
|
||||
let dimNodeTransition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: transitionCurve)
|
||||
dimNodeTransition.updateAlpha(node: self.dimNode, alpha: 0.0)
|
||||
} else {
|
||||
performCompletion()
|
||||
}
|
||||
}
|
||||
|
||||
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
|
||||
if self.isAnimatingOut {
|
||||
self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset.y)
|
||||
transition.animateOffsetAdditive(node: self.scrollingContainer, offset: -offset.y)
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public final class PinchController: ViewController, StandalonePresentableController {
|
||||
@ -335,6 +410,7 @@ public final class PinchController: ViewController, StandalonePresentableControl
|
||||
}
|
||||
|
||||
private let sourceNode: PinchSourceContainerNode
|
||||
private let getContentAreaInScreenSpace: () -> CGRect
|
||||
|
||||
private var wasDismissed = false
|
||||
|
||||
@ -342,8 +418,9 @@ public final class PinchController: ViewController, StandalonePresentableControl
|
||||
return self.displayNode as! PinchControllerNode
|
||||
}
|
||||
|
||||
public init(sourceNode: PinchSourceContainerNode) {
|
||||
public init(sourceNode: PinchSourceContainerNode, getContentAreaInScreenSpace: @escaping () -> CGRect) {
|
||||
self.sourceNode = sourceNode
|
||||
self.getContentAreaInScreenSpace = getContentAreaInScreenSpace
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
@ -361,7 +438,7 @@ public final class PinchController: ViewController, StandalonePresentableControl
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = PinchControllerNode(controller: self, sourceNode: self.sourceNode)
|
||||
self.displayNode = PinchControllerNode(controller: self, sourceNode: self.sourceNode, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace)
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
@ -392,4 +469,8 @@ public final class PinchController: ViewController, StandalonePresentableControl
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
|
||||
self.controllerNode.addRelativeContentOffset(offset, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
case skipReadHistory(PresentationTheme, Bool)
|
||||
case crashOnSlowQueries(PresentationTheme, Bool)
|
||||
case clearTips(PresentationTheme)
|
||||
case reimport(PresentationTheme)
|
||||
case crash(PresentationTheme)
|
||||
case resetData(PresentationTheme)
|
||||
case resetDatabase(PresentationTheme)
|
||||
case resetDatabaseAndCache(PresentationTheme)
|
||||
@ -74,6 +74,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
case knockoutWallpaper(PresentationTheme, Bool)
|
||||
case demoVideoChats(Bool)
|
||||
case experimentalCompatibility(Bool)
|
||||
case enableNoiseSuppression(Bool)
|
||||
case playerEmbedding(Bool)
|
||||
case playlistPlayback(Bool)
|
||||
case voiceConference
|
||||
@ -93,7 +94,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return DebugControllerSection.logging.rawValue
|
||||
case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .clearTips, .reimport, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .demoVideoChats, .experimentalCompatibility, .playerEmbedding, .playlistPlayback, .voiceConference:
|
||||
case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .demoVideoChats, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableNoiseSuppression:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .preferredVideoCodec:
|
||||
return DebugControllerSection.videoExperiments.rawValue
|
||||
@ -134,7 +135,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return 12
|
||||
case .clearTips:
|
||||
return 13
|
||||
case .reimport:
|
||||
case .crash:
|
||||
return 14
|
||||
case .resetData:
|
||||
return 15
|
||||
@ -158,14 +159,16 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return 24
|
||||
case .experimentalCompatibility:
|
||||
return 25
|
||||
case .playerEmbedding:
|
||||
case .enableNoiseSuppression:
|
||||
return 26
|
||||
case .playlistPlayback:
|
||||
case .playerEmbedding:
|
||||
return 27
|
||||
case .voiceConference:
|
||||
case .playlistPlayback:
|
||||
return 28
|
||||
case .voiceConference:
|
||||
return 29
|
||||
case let .preferredVideoCodec(index, _, _, _):
|
||||
return 29 + index
|
||||
return 30 + index
|
||||
case .disableVideoAspectScaling:
|
||||
return 100
|
||||
case .enableVoipTcp:
|
||||
@ -553,20 +556,9 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
}).start()
|
||||
}
|
||||
})
|
||||
case let .reimport(theme):
|
||||
return ItemListActionItem(presentationData: presentationData, title: "Reimport Application Data", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
let appGroupName = "group.\(Bundle.main.bundleIdentifier!)"
|
||||
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
||||
|
||||
guard let appGroupUrl = maybeAppGroupUrl else {
|
||||
return
|
||||
}
|
||||
|
||||
let statusPath = appGroupUrl.path + "/Documents/importcompleted"
|
||||
if FileManager.default.fileExists(atPath: statusPath) {
|
||||
let _ = try? FileManager.default.removeItem(at: URL(fileURLWithPath: statusPath))
|
||||
exit(0)
|
||||
}
|
||||
case let .crash(theme):
|
||||
return ItemListActionItem(presentationData: presentationData, title: "Crash", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
preconditionFailure()
|
||||
})
|
||||
case let .resetData(theme):
|
||||
return ItemListActionItem(presentationData: presentationData, title: "Reset Data", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
@ -725,6 +717,16 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .enableNoiseSuppression(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Noise Suppression", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
|
||||
var settings = settings as? ExperimentalUISettings ?? ExperimentalUISettings.defaultSettings
|
||||
settings.enableNoiseSuppression = value
|
||||
return settings
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .playerEmbedding(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
@ -822,6 +824,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
|
||||
if isMainApp {
|
||||
entries.append(.clearTips(presentationData.theme))
|
||||
}
|
||||
entries.append(.crash(presentationData.theme))
|
||||
entries.append(.resetData(presentationData.theme))
|
||||
entries.append(.resetDatabase(presentationData.theme))
|
||||
entries.append(.resetDatabaseAndCache(presentationData.theme))
|
||||
@ -834,6 +837,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
|
||||
entries.append(.knockoutWallpaper(presentationData.theme, experimentalSettings.knockoutWallpaper))
|
||||
entries.append(.demoVideoChats(experimentalSettings.demoVideoChats))
|
||||
entries.append(.experimentalCompatibility(experimentalSettings.experimentalCompatibility))
|
||||
entries.append(.enableNoiseSuppression(experimentalSettings.enableNoiseSuppression))
|
||||
entries.append(.playerEmbedding(experimentalSettings.playerEmbedding))
|
||||
entries.append(.playlistPlayback(experimentalSettings.playlistPlayback))
|
||||
}
|
||||
|
||||
@ -563,7 +563,13 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
guard let strongSelf = self, let controller = strongSelf.controller else {
|
||||
return
|
||||
}
|
||||
let pinchController = PinchController(sourceNode: sourceNode)
|
||||
let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: {
|
||||
guard let strongSelf = self, let controller = strongSelf.controller else {
|
||||
return CGRect()
|
||||
}
|
||||
|
||||
return controller.view.convert(controller.view.bounds, to: nil)
|
||||
})
|
||||
controller.window?.presentInGlobalOverlay(pinchController)
|
||||
}, openPeer: { [weak self] peerId in
|
||||
self?.openPeer(peerId)
|
||||
|
||||
@ -235,9 +235,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil)
|
||||
let path = basePath + "/db_sqlite"
|
||||
|
||||
#if DEBUG
|
||||
print("Instance \(self) opening sqlite at \(path)")
|
||||
#endif
|
||||
postboxLog("Instance \(self) opening sqlite at \(path)")
|
||||
|
||||
#if DEBUG
|
||||
let exists = FileManager.default.fileExists(atPath: path)
|
||||
@ -298,7 +296,9 @@ public final class SqliteValueBox: ValueBox {
|
||||
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
|
||||
|
||||
@ -307,7 +307,11 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultCode = database.execute("PRAGMA cipher_default_plaintext_header_size=32")
|
||||
assert(resultCode)
|
||||
|
||||
postboxLog("Did set up cipher")
|
||||
|
||||
if self.isEncrypted(database) {
|
||||
postboxLog("Database is encrypted")
|
||||
|
||||
if let encryptionParameters = encryptionParameters {
|
||||
precondition(encryptionParameters.salt.data.count == 16)
|
||||
precondition(encryptionParameters.key.data.count == 32)
|
||||
@ -317,11 +321,14 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultCode = database.execute("PRAGMA key=\"x'\(hexKey)'\"")
|
||||
assert(resultCode)
|
||||
|
||||
postboxLog("Setting encryption key")
|
||||
|
||||
if self.isEncrypted(database) {
|
||||
postboxLog("Encryption key is invalid")
|
||||
|
||||
if isTemporary || isReadOnly {
|
||||
return nil
|
||||
}
|
||||
postboxLog("Encryption key is invalid")
|
||||
|
||||
for fileName in dabaseFileNames {
|
||||
let _ = try? FileManager.default.removeItem(atPath: basePath + "/\(fileName)")
|
||||
@ -354,6 +361,8 @@ public final class SqliteValueBox: ValueBox {
|
||||
assert(resultCode)
|
||||
}
|
||||
} else if let encryptionParameters = encryptionParameters, encryptionParameters.forceEncryptionIfNoSet {
|
||||
postboxLog("Not encrypted")
|
||||
|
||||
let hexKey = hexString(encryptionParameters.key.data + encryptionParameters.salt.data)
|
||||
|
||||
if FileManager.default.fileExists(atPath: path) {
|
||||
@ -410,6 +419,8 @@ public final class SqliteValueBox: ValueBox {
|
||||
}
|
||||
}
|
||||
|
||||
postboxLog("Did set up encryption")
|
||||
|
||||
//database.execute("PRAGMA cache_size=-2097152")
|
||||
resultCode = database.execute("PRAGMA mmap_size=0")
|
||||
assert(resultCode)
|
||||
@ -421,6 +432,9 @@ public final class SqliteValueBox: ValueBox {
|
||||
assert(resultCode)
|
||||
resultCode = database.execute("PRAGMA cipher_memory_security = OFF")
|
||||
assert(resultCode)
|
||||
|
||||
postboxLog("Did set up pragmas")
|
||||
|
||||
//resultCode = database.execute("PRAGMA wal_autocheckpoint=500")
|
||||
//database.execute("PRAGMA journal_size_limit=1536")
|
||||
|
||||
@ -442,8 +456,12 @@ public final class SqliteValueBox: ValueBox {
|
||||
let _ = self.runPragma(database, "checkpoint_fullfsync = 1")
|
||||
assert(self.runPragma(database, "checkpoint_fullfsync") == "1")
|
||||
|
||||
postboxLog("Did set up checkpoint_fullfsync")
|
||||
|
||||
self.beginInternal(database: database)
|
||||
|
||||
postboxLog("Did begin transaction")
|
||||
|
||||
let result = self.getUserVersion(database)
|
||||
|
||||
if result < 3 {
|
||||
@ -463,8 +481,12 @@ public final class SqliteValueBox: ValueBox {
|
||||
self.fullTextTables[table.id] = table
|
||||
}
|
||||
|
||||
postboxLog("Did load tables")
|
||||
|
||||
self.commitInternal(database: database)
|
||||
|
||||
postboxLog("Did commit final")
|
||||
|
||||
lock.unlock()
|
||||
|
||||
return database
|
||||
@ -518,7 +540,21 @@ public final class SqliteValueBox: ValueBox {
|
||||
|
||||
private func isEncrypted(_ database: Database) -> Bool {
|
||||
var statement: OpaquePointer? = nil
|
||||
postboxLog("isEncrypted prepare...")
|
||||
|
||||
let allIsOk = Atomic<Bool>(value: false)
|
||||
let databasePath = self.databasePath
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 5.0, execute: {
|
||||
if allIsOk.with({ $0 }) == false {
|
||||
postboxLog("Timeout reached, discarding database")
|
||||
try? FileManager.default.removeItem(atPath: databasePath)
|
||||
|
||||
exit(0)
|
||||
}
|
||||
})
|
||||
let status = sqlite3_prepare_v2(database.handle, "SELECT * FROM sqlite_master LIMIT 1", -1, &statement, nil)
|
||||
let _ = allIsOk.swap(true)
|
||||
postboxLog("isEncrypted prepare done")
|
||||
if statement == nil {
|
||||
postboxLog("isEncrypted: sqlite3_prepare_v2 status = \(status) [\(self.databasePath)]")
|
||||
return true
|
||||
@ -536,6 +572,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
preparedStatement.destroy()
|
||||
return true
|
||||
}
|
||||
postboxLog("isEncrypted step done")
|
||||
preparedStatement.destroy()
|
||||
return status == SQLITE_NOTADB
|
||||
}
|
||||
|
||||
@ -347,7 +347,7 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
||||
var title: String = ""
|
||||
var speakerSubtitle: String = ""
|
||||
|
||||
let textFont = Font.regular(13.0)
|
||||
let textFont = Font.with(size: 13.0, design: .regular, weight: .regular, traits: [.monospacedNumbers])
|
||||
let textColor = UIColor.white
|
||||
var segments: [AnimatedCountLabelNode.Segment] = []
|
||||
var displaySpeakerSubtitle = false
|
||||
@ -381,7 +381,22 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
||||
}
|
||||
displaySpeakerSubtitle = speakerSubtitle != title && !speakerSubtitle.isEmpty
|
||||
|
||||
if let membersCount = membersCount {
|
||||
var requiresTimer = false
|
||||
if let scheduleTime = self.currentGroupCallState?.info?.scheduleTimestamp {
|
||||
requiresTimer = true
|
||||
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
let elapsedTime = scheduleTime - currentTime
|
||||
let timerText: String
|
||||
if elapsedTime >= 86400 {
|
||||
timerText = timeIntervalString(strings: presentationData.strings, value: elapsedTime)
|
||||
} else if elapsedTime < 0 {
|
||||
timerText = presentationData.strings.VoiceChat_StatusLateBy(textForTimeout(value: abs(elapsedTime))).0
|
||||
} else {
|
||||
timerText = presentationData.strings.VoiceChat_StatusStartsIn(textForTimeout(value: elapsedTime)).0
|
||||
}
|
||||
segments.append(.text(0, NSAttributedString(string: timerText, font: textFont, textColor: textColor)))
|
||||
} else if let membersCount = membersCount {
|
||||
var membersPart = presentationData.strings.VoiceChat_Status_Members(membersCount)
|
||||
if membersPart.contains("[") && membersPart.contains("]") {
|
||||
if let startIndex = membersPart.firstIndex(of: "["), let endIndex = membersPart.firstIndex(of: "]") {
|
||||
@ -433,6 +448,19 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
||||
}
|
||||
|
||||
self.backgroundNode.connectingColor = color
|
||||
|
||||
if requiresTimer {
|
||||
if self.currentCallTimer == nil {
|
||||
let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
|
||||
self?.update()
|
||||
}, queue: Queue.mainQueue())
|
||||
timer.start()
|
||||
self.currentCallTimer = timer
|
||||
}
|
||||
} else if let currentCallTimer = self.currentCallTimer {
|
||||
self.currentCallTimer = nil
|
||||
currentCallTimer.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
if self.subtitleNode.segments != segments && !displaySpeakerSubtitle {
|
||||
|
||||
@ -525,7 +525,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
if elapsedTime >= 86400 {
|
||||
joinText = timeIntervalString(strings: strings, value: elapsedTime)
|
||||
} else if elapsedTime < 0 {
|
||||
joinText = "+\(textForTimeout(value: abs(elapsedTime)))"
|
||||
joinText = "-\(textForTimeout(value: abs(elapsedTime)))"
|
||||
} else {
|
||||
joinText = textForTimeout(value: elapsedTime)
|
||||
}
|
||||
|
||||
@ -557,6 +557,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
private var peerUpdatesSubscription: Disposable?
|
||||
|
||||
public private(set) var schedulePending = false
|
||||
private var isScheduled = false
|
||||
private var isScheduledStarted = false
|
||||
|
||||
init(
|
||||
accountContext: AccountContext,
|
||||
@ -581,6 +583,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.invite = invite
|
||||
self.joinAsPeerId = joinAsPeerId ?? accountContext.account.peerId
|
||||
self.schedulePending = initialCall == nil
|
||||
self.isScheduled = initialCall == nil || initialCall?.scheduleTimestamp != nil
|
||||
|
||||
self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title, scheduleTimestamp: initialCall?.scheduleTimestamp, subscribedToScheduled: initialCall?.subscribedToScheduled ?? false)
|
||||
self.statePromise = ValuePromise(self.stateValue)
|
||||
@ -1058,6 +1061,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
case let .active(previousCallInfo):
|
||||
if case let .active(callInfo) = internalState {
|
||||
shouldJoin = previousCallInfo.scheduleTimestamp != nil && callInfo.scheduleTimestamp == nil
|
||||
self.participantsContext = nil
|
||||
activeCallInfo = callInfo
|
||||
} else {
|
||||
activeCallInfo = nil
|
||||
@ -1082,6 +1086,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
outgoingAudioBitrateKbit = value
|
||||
}
|
||||
|
||||
let enableNoiseSuppression = accountContext.sharedContext.immediateExperimentalUISettings.enableNoiseSuppression
|
||||
|
||||
callContext = OngoingGroupCallContext(video: self.videoCapturer, participantDescriptionsRequired: { [weak self] ssrcs in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
@ -1098,7 +1104,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
strongSelf.requestCall(movingFromBroadcastToRtc: false)
|
||||
}
|
||||
}
|
||||
}, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: self.isVideo)
|
||||
}, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: self.isVideo, enableNoiseSuppression: enableNoiseSuppression)
|
||||
self.incomingVideoSourcePromise.set(callContext.videoSources
|
||||
|> deliverOnMainQueue
|
||||
|> map { [weak self] sources -> [PeerId: UInt32] in
|
||||
@ -1284,10 +1290,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
if !strongSelf.didConnectOnce {
|
||||
strongSelf.didConnectOnce = true
|
||||
|
||||
if !strongSelf.isScheduled {
|
||||
let toneRenderer = PresentationCallToneRenderer(tone: .groupJoined)
|
||||
strongSelf.toneRenderer = toneRenderer
|
||||
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
|
||||
}
|
||||
}
|
||||
|
||||
if let peer = strongSelf.reconnectingAsPeer {
|
||||
strongSelf.reconnectingAsPeer = nil
|
||||
@ -1711,6 +1719,180 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
if let isCurrentlyConnecting = self.isCurrentlyConnecting, isCurrentlyConnecting {
|
||||
self.startCheckingCallIfNeeded()
|
||||
}
|
||||
} else if case let .active(callInfo) = internalState, callInfo.scheduleTimestamp != nil {
|
||||
let accountContext = self.accountContext
|
||||
let peerId = self.peerId
|
||||
let rawAdminIds: Signal<Set<PeerId>, NoError>
|
||||
if peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||
rawAdminIds = Signal { subscriber in
|
||||
let (disposable, _) = accountContext.peerChannelMemberCategoriesContextsManager.admins(postbox: accountContext.account.postbox, network: accountContext.account.network, accountPeerId: accountContext.account.peerId, peerId: peerId, updated: { list in
|
||||
var peerIds = Set<PeerId>()
|
||||
for item in list.list {
|
||||
if let adminInfo = item.participant.adminInfo, adminInfo.rights.rights.contains(.canManageCalls) {
|
||||
peerIds.insert(item.peer.id)
|
||||
}
|
||||
}
|
||||
subscriber.putNext(peerIds)
|
||||
})
|
||||
return disposable
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|> runOn(.mainQueue())
|
||||
} else {
|
||||
rawAdminIds = accountContext.account.postbox.combinedView(keys: [.cachedPeerData(peerId: peerId)])
|
||||
|> map { views -> Set<PeerId> in
|
||||
guard let view = views.views[.cachedPeerData(peerId: peerId)] as? CachedPeerDataView else {
|
||||
return Set()
|
||||
}
|
||||
guard let cachedData = view.cachedPeerData as? CachedGroupData, let participants = cachedData.participants else {
|
||||
return Set()
|
||||
}
|
||||
return Set(participants.participants.compactMap { item -> PeerId? in
|
||||
switch item {
|
||||
case .creator, .admin:
|
||||
return item.peerId
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
}
|
||||
|
||||
let adminIds = combineLatest(queue: .mainQueue(),
|
||||
rawAdminIds,
|
||||
accountContext.account.postbox.combinedView(keys: [.basicPeer(peerId)])
|
||||
)
|
||||
|> map { rawAdminIds, view -> Set<PeerId> in
|
||||
var rawAdminIds = rawAdminIds
|
||||
if let peerView = view.views[.basicPeer(peerId)] as? BasicPeerView, let peer = peerView.peer as? TelegramChannel {
|
||||
if peer.hasPermission(.manageCalls) {
|
||||
rawAdminIds.insert(accountContext.account.peerId)
|
||||
} else {
|
||||
rawAdminIds.remove(accountContext.account.peerId)
|
||||
}
|
||||
}
|
||||
return rawAdminIds
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let participantsContext = GroupCallParticipantsContext(
|
||||
account: self.accountContext.account,
|
||||
peerId: self.peerId,
|
||||
myPeerId: self.joinAsPeerId,
|
||||
id: callInfo.id,
|
||||
accessHash: callInfo.accessHash,
|
||||
state: GroupCallParticipantsContext.State(
|
||||
participants: [],
|
||||
nextParticipantsFetchOffset: nil,
|
||||
adminIds: Set(),
|
||||
isCreator: false,
|
||||
defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: self.stateValue.defaultParticipantMuteState == .muted, canChange: false),
|
||||
sortAscending: true,
|
||||
recordingStartTimestamp: nil,
|
||||
title: self.stateValue.title,
|
||||
scheduleTimestamp: self.stateValue.scheduleTimestamp,
|
||||
subscribedToScheduled: self.stateValue.subscribedToScheduled,
|
||||
totalCount: 0,
|
||||
version: 0
|
||||
),
|
||||
previousServiceState: nil
|
||||
)
|
||||
self.temporaryParticipantsContext = nil
|
||||
self.participantsContext = participantsContext
|
||||
|
||||
let myPeerId = self.joinAsPeerId
|
||||
let myPeer = self.accountContext.account.postbox.transaction { transaction -> (Peer, CachedPeerData?)? in
|
||||
if let peer = transaction.getPeer(myPeerId) {
|
||||
return (peer, transaction.getPeerCachedData(peerId: myPeerId))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(),
|
||||
participantsContext.state,
|
||||
adminIds,
|
||||
myPeer,
|
||||
accountContext.account.postbox.peerView(id: peerId)
|
||||
).start(next: { [weak self] state, adminIds, myPeerAndCachedData, view in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
var members = PresentationGroupCallMembers(
|
||||
participants: [],
|
||||
speakingParticipants: Set(),
|
||||
totalCount: state.totalCount,
|
||||
loadMoreToken: state.nextParticipantsFetchOffset
|
||||
)
|
||||
|
||||
var participants: [GroupCallParticipantsContext.Participant] = []
|
||||
var topParticipants: [GroupCallParticipantsContext.Participant] = []
|
||||
if let (myPeer, cachedData) = myPeerAndCachedData {
|
||||
let about: String?
|
||||
if let cachedData = cachedData as? CachedUserData {
|
||||
about = cachedData.about
|
||||
} else if let cachedData = cachedData as? CachedUserData {
|
||||
about = cachedData.about
|
||||
} else {
|
||||
about = nil
|
||||
}
|
||||
participants.append(GroupCallParticipantsContext.Participant(
|
||||
peer: myPeer,
|
||||
ssrc: nil,
|
||||
jsonParams: nil,
|
||||
joinTimestamp: strongSelf.temporaryJoinTimestamp,
|
||||
raiseHandRating: strongSelf.temporaryRaiseHandRating,
|
||||
hasRaiseHand: strongSelf.temporaryHasRaiseHand,
|
||||
activityTimestamp: strongSelf.temporaryActivityTimestamp,
|
||||
activityRank: strongSelf.temporaryActivityRank,
|
||||
muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
|
||||
volume: nil,
|
||||
about: about
|
||||
))
|
||||
}
|
||||
|
||||
for participant in participants {
|
||||
members.participants.append(participant)
|
||||
|
||||
if topParticipants.count < 3 {
|
||||
topParticipants.append(participant)
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.membersValue = members
|
||||
strongSelf.stateValue.adminIds = adminIds
|
||||
strongSelf.stateValue.canManageCall = state.isCreator || adminIds.contains(strongSelf.accountContext.account.peerId)
|
||||
if (state.isCreator || strongSelf.stateValue.adminIds.contains(strongSelf.accountContext.account.peerId)) && state.defaultParticipantsAreMuted.canChange {
|
||||
strongSelf.stateValue.defaultParticipantMuteState = state.defaultParticipantsAreMuted.isMuted ? .muted : .unmuted
|
||||
}
|
||||
strongSelf.stateValue.recordingStartTimestamp = state.recordingStartTimestamp
|
||||
strongSelf.stateValue.title = state.title
|
||||
|
||||
strongSelf.stateValue.scheduleTimestamp = strongSelf.isScheduledStarted ? nil : state.scheduleTimestamp
|
||||
if state.scheduleTimestamp == nil && !strongSelf.isScheduledStarted {
|
||||
strongSelf.updateSessionState(internalState: .active(GroupCallInfo(id: callInfo.id, accessHash: callInfo.accessHash, participantCount: state.totalCount, clientParams: callInfo.clientParams, streamDcId: callInfo.streamDcId, title: state.title, scheduleTimestamp: nil, subscribedToScheduled: false, recordingStartTimestamp: nil, sortAscending: true)), audioSessionControl: strongSelf.audioSessionControl)
|
||||
} else {
|
||||
strongSelf.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo(
|
||||
id: callInfo.id,
|
||||
accessHash: callInfo.accessHash,
|
||||
participantCount: state.totalCount,
|
||||
clientParams: nil,
|
||||
streamDcId: nil,
|
||||
title: state.title,
|
||||
scheduleTimestamp: state.scheduleTimestamp,
|
||||
subscribedToScheduled: false,
|
||||
recordingStartTimestamp: state.recordingStartTimestamp,
|
||||
sortAscending: state.sortAscending
|
||||
))))
|
||||
|
||||
strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState(
|
||||
participantCount: state.totalCount,
|
||||
topParticipants: topParticipants,
|
||||
activeSpeakers: Set()
|
||||
)))
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1886,18 +2068,22 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
return transaction.getPeer(peerId)
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] myPeer in
|
||||
guard let strongSelf = self, let _ = myPeer else {
|
||||
guard let strongSelf = self, let myPeer = myPeer else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.reconnectingAsPeer = myPeer
|
||||
|
||||
let previousPeerId = strongSelf.joinAsPeerId
|
||||
if let localSsrc = strongSelf.currentLocalSsrc {
|
||||
strongSelf.ignorePreviousJoinAsPeerId = (previousPeerId, localSsrc)
|
||||
}
|
||||
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 {
|
||||
for participant in immediateState.participants {
|
||||
if participant.peer.id == previousPeerId {
|
||||
@ -1915,6 +2101,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
|
||||
strongSelf.requestCall(movingFromBroadcastToRtc: false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -2011,20 +2198,32 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.schedulePending = false
|
||||
self.stateValue.scheduleTimestamp = timestamp
|
||||
|
||||
self.summaryParticipantsState.set(.single(SummaryParticipantsState(
|
||||
participantCount: 1,
|
||||
topParticipants: [],
|
||||
activeSpeakers: Set()
|
||||
)))
|
||||
|
||||
self.startDisposable.set((createGroupCall(account: self.account, peerId: self.peerId, title: nil, scheduleDate: timestamp)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] callInfo in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateSessionState(internalState: .active(callInfo), audioSessionControl: strongSelf.audioSessionControl)
|
||||
}, error: { [weak self] error in
|
||||
if let strongSelf = self {
|
||||
strongSelf.markAsCanBeRemoved()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
public func startScheduled() {
|
||||
guard case let .active(callInfo) = self.internalState else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isScheduledStarted = true
|
||||
self.stateValue.scheduleTimestamp = nil
|
||||
|
||||
self.startDisposable.set((startScheduledGroupCall(account: self.account, peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash)
|
||||
@ -2033,6 +2232,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
return
|
||||
}
|
||||
strongSelf.updateSessionState(internalState: .active(callInfo), audioSessionControl: strongSelf.audioSessionControl)
|
||||
|
||||
let toneRenderer = PresentationCallToneRenderer(tone: .groupJoined)
|
||||
strongSelf.toneRenderer = toneRenderer
|
||||
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
|
||||
}))
|
||||
}
|
||||
|
||||
@ -2244,7 +2447,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
|
||||
let account = self.account
|
||||
|
||||
let currentCall: Signal<GroupCallInfo?, CallError>
|
||||
if let initialCall = self.initialCall {
|
||||
currentCall = getCurrentGroupCall(account: account, callId: initialCall.id, accessHash: initialCall.accessHash)
|
||||
@ -2254,6 +2456,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
|> map { summary -> GroupCallInfo? in
|
||||
return summary?.info
|
||||
}
|
||||
} else if case let .active(callInfo) = self.internalState {
|
||||
currentCall = getCurrentGroupCall(account: account, callId: callInfo.id, accessHash: callInfo.accessHash)
|
||||
|> mapError { _ -> CallError in
|
||||
return .generic
|
||||
}
|
||||
|> map { summary -> GroupCallInfo? in
|
||||
return summary?.info
|
||||
}
|
||||
} else {
|
||||
currentCall = .single(nil)
|
||||
}
|
||||
@ -2319,7 +2529,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
guard let callInfo = self.internalState.callInfo else {
|
||||
return
|
||||
}
|
||||
self.stateValue.title = title
|
||||
self.stateValue.title = title.isEmpty ? nil : title
|
||||
let _ = editGroupCallTitle(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, title: title).start()
|
||||
}
|
||||
|
||||
|
||||
@ -242,7 +242,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
let subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
|
||||
let totalHeight = titleSize.height + subtitleSize.height + 1.0
|
||||
|
||||
self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - totalHeight) / 2.0) + 88.0), size: titleSize)
|
||||
self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - totalHeight) / 2.0) + 84.0), size: titleSize)
|
||||
self.subtitleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: self.titleLabel.frame.maxY + 1.0), size: subtitleSize)
|
||||
|
||||
self.bottomNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
@ -361,6 +361,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
}
|
||||
|
||||
var backgroundState: VoiceChatActionButtonBackgroundNode.State
|
||||
var animated = true
|
||||
switch state {
|
||||
case let .button(text):
|
||||
backgroundState = .button
|
||||
@ -370,6 +371,9 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
self.buttonTitleLabel.frame = CGRect(origin: CGPoint(x: floor((self.bounds.width - titleSize.width) / 2.0), y: floor((self.bounds.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
case .scheduled:
|
||||
backgroundState = .disabled
|
||||
if previousState == .connecting {
|
||||
animated = false
|
||||
}
|
||||
case let .active(state):
|
||||
switch state {
|
||||
case .on:
|
||||
@ -385,7 +389,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
||||
self.applyIconParams()
|
||||
|
||||
self.backgroundNode.isDark = dark
|
||||
self.backgroundNode.update(state: backgroundState, animated: true)
|
||||
self.backgroundNode.update(state: backgroundState, animated: animated)
|
||||
|
||||
if case .active = state, let previousState = previousState, case .connecting = previousState, animated {
|
||||
self.activeDisposable.set((self.activePromise.get()
|
||||
@ -755,7 +759,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
||||
case muted
|
||||
}
|
||||
|
||||
func updateGlowAndGradientAnimations(type: Gradient, previousType: Gradient? = nil) {
|
||||
func updateGlowAndGradientAnimations(type: Gradient, previousType: Gradient? = nil, animated: Bool = true) {
|
||||
let effectivePreviousTyoe = previousType ?? .active
|
||||
|
||||
let scale: CGFloat
|
||||
@ -794,13 +798,15 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
||||
self.maskGradientLayer.transform = CATransform3DMakeScale(targetScale, targetScale, 1.0)
|
||||
if let _ = previousType {
|
||||
self.maskGradientLayer.animateScale(from: initialScale, to: targetScale, duration: 0.3)
|
||||
} else {
|
||||
} else if animated {
|
||||
self.maskGradientLayer.animateSpring(from: initialScale as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", duration: 0.45)
|
||||
}
|
||||
|
||||
self.foregroundGradientLayer.colors = targetColors
|
||||
if animated {
|
||||
self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
private func playMuteAnimation() {
|
||||
self.maskBlobView.startAnimating()
|
||||
@ -1081,6 +1087,16 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
||||
self.playMuteAnimation()
|
||||
}
|
||||
self.transition = nil
|
||||
} else {
|
||||
if self.maskBlobView.isHidden {
|
||||
self.updateGlowAndGradientAnimations(type: .muted, previousType: nil, animated: false)
|
||||
self.maskCircleLayer.isHidden = false
|
||||
self.maskProgressLayer.isHidden = true
|
||||
self.maskGradientLayer.isHidden = false
|
||||
self.maskBlobView.isHidden = false
|
||||
self.maskBlobView.startAnimating()
|
||||
self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45)
|
||||
}
|
||||
}
|
||||
case .button:
|
||||
self.updatedActive?(true)
|
||||
@ -1537,7 +1553,6 @@ final class VoiceChatActionButtonIconNode: ManagedAnimationNode {
|
||||
self.isColored = isColored
|
||||
super.init(size: CGSize(width: 100.0, height: 100.0))
|
||||
|
||||
self.scale = 0.8
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.1))
|
||||
}
|
||||
|
||||
@ -1632,15 +1647,25 @@ final class VoiceChatActionButtonIconNode: ManagedAnimationNode {
|
||||
}
|
||||
|
||||
var useTiredAnimation = false
|
||||
var useAngryAnimation = false
|
||||
let val = Float.random(in: 0.0..<1.0)
|
||||
if val <= 0.01 {
|
||||
useTiredAnimation = true
|
||||
} else if val <= 0.05 {
|
||||
useAngryAnimation = true
|
||||
}
|
||||
|
||||
let normalAnimations = ["VoiceHand_1", "VoiceHand_2", "VoiceHand_3", "VoiceHand_4", "VoiceHand_7"]
|
||||
let normalAnimations = ["VoiceHand_1", "VoiceHand_2", "VoiceHand_3", "VoiceHand_4", "VoiceHand_7", "VoiceHand_8"]
|
||||
let tiredAnimations = ["VoiceHand_5", "VoiceHand_6"]
|
||||
let animations = useTiredAnimation ? tiredAnimations : normalAnimations
|
||||
|
||||
let angryAnimations = ["VoiceHand_9", "VoiceHand_10"]
|
||||
let animations: [String]
|
||||
if useTiredAnimation {
|
||||
animations = tiredAnimations
|
||||
} else if useAngryAnimation {
|
||||
animations = angryAnimations
|
||||
} else {
|
||||
animations = normalAnimations
|
||||
}
|
||||
if let animationName = animations.randomElement() {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local(animationName)))
|
||||
}
|
||||
|
||||
@ -830,6 +830,9 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
self.scheduleTextNode = ImmediateTextNode()
|
||||
self.scheduleTextNode.isHidden = !self.isScheduling
|
||||
self.scheduleTextNode.isUserInteractionEnabled = false
|
||||
self.scheduleTextNode.textAlignment = .center
|
||||
self.scheduleTextNode.maximumNumberOfLines = 4
|
||||
|
||||
self.scheduleCancelButton = SolidRoundedButtonNode(title: self.presentationData.strings.Common_Cancel, theme: SolidRoundedButtonTheme(backgroundColor: UIColor(rgb: 0x2b2b2f), foregroundColor: .white), height: 52.0, cornerRadius: 10.0)
|
||||
self.scheduleCancelButton.isHidden = !self.isScheduling
|
||||
@ -840,6 +843,7 @@ public final class VoiceChatController: ViewController {
|
||||
self.dateFormatter.timeZone = TimeZone.current
|
||||
|
||||
self.timerNode = VoiceChatTimerNode(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat)
|
||||
self.timerNode.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
@ -1258,10 +1262,17 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditBioTitle, text: presentationData.strings.VoiceChat_EditBioText, placeholder: presentationData.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: presentationData.strings.VoiceChat_EditBioSave, value: entry.about, maxLength: maxBioLength, apply: { bio in
|
||||
if let strongSelf = self, let bio = bio {
|
||||
if peer.id.namespace == Namespaces.Peer.CloudUser {
|
||||
let _ = (updateAbout(account: strongSelf.context.account, about: bio)
|
||||
|> `catch` { _ -> Signal<Void, NoError> in
|
||||
return .complete()
|
||||
}).start()
|
||||
} else {
|
||||
let _ = (updatePeerTitle(account: strongSelf.context.account, peerId: peer.id, title: bio)
|
||||
|> `catch` { _ -> Signal<Void, NoError> in
|
||||
return .complete()
|
||||
}).start()
|
||||
}
|
||||
|
||||
strongSelf.presentUndoOverlay(content: .info(text: strongSelf.presentationData.strings.VoiceChat_EditBioSuccess), action: { _ in return false })
|
||||
}
|
||||
@ -1501,6 +1512,7 @@ public final class VoiceChatController: ViewController {
|
||||
self.contentContainer.addSubnode(self.rightBorderNode)
|
||||
self.contentContainer.addSubnode(self.bottomPanelNode)
|
||||
self.contentContainer.addSubnode(self.timerNode)
|
||||
self.contentContainer.addSubnode(self.scheduleTextNode)
|
||||
|
||||
let invitedPeers: Signal<[Peer], NoError> = self.call.invitedPeers
|
||||
|> mapToSignal { ids -> Signal<[Peer], NoError> in
|
||||
@ -1700,7 +1712,7 @@ public final class VoiceChatController: ViewController {
|
||||
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in
|
||||
if let strongSelf = self {
|
||||
strongSelf.currentContentOffset = offset
|
||||
if strongSelf.animation == nil && !strongSelf.animatingExpansion {
|
||||
if !strongSelf.animatingExpansion && !strongSelf.animatingInsertion && strongSelf.panGestureArguments == nil {
|
||||
strongSelf.updateFloatingHeaderOffset(offset: offset, transition: transition)
|
||||
}
|
||||
}
|
||||
@ -1994,7 +2006,7 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.isNoiseSuppressionEnabled ? "Disable Noise Suppression" : "Enable Noise Suppression", textColor: .primary, icon: { theme in
|
||||
/*items.append(.action(ContextMenuActionItem(text: strongSelf.isNoiseSuppressionEnabled ? "Disable Noise Suppression" : "Enable Noise Suppression", textColor: .primary, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
@ -2004,7 +2016,7 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
strongSelf.call.setIsNoiseSuppressionEnabled(!strongSelf.isNoiseSuppressionEnabled)
|
||||
})))
|
||||
})))*/
|
||||
|
||||
if let callState = strongSelf.callState, callState.canManageCall {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EndVoiceChat, textColor: .destructive, icon: { theme in
|
||||
@ -2206,7 +2218,7 @@ public final class VoiceChatController: ViewController {
|
||||
self.view.addGestureRecognizer(panRecognizer)
|
||||
|
||||
if self.isScheduling {
|
||||
self.setupPickerView()
|
||||
self.setupSchedulePickerView()
|
||||
self.updateScheduleButtonTitle()
|
||||
}
|
||||
}
|
||||
@ -2218,22 +2230,27 @@ public final class VoiceChatController: ViewController {
|
||||
let currentDate = Date()
|
||||
var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: currentDate)
|
||||
components.second = 0
|
||||
let minute = (components.minute ?? 0) % 5
|
||||
|
||||
let next1MinDate = calendar.date(byAdding: .minute, value: 1, to: calendar.date(from: components)!)
|
||||
let next5MinDate = calendar.date(byAdding: .minute, value: 5 - minute, to: calendar.date(from: components)!)
|
||||
let roundedDate = calendar.date(from: components)!
|
||||
let next1MinDate = calendar.date(byAdding: .minute, value: 1, to: roundedDate)
|
||||
|
||||
components.minute = 0
|
||||
let roundedToHourDate = calendar.date(from: components)!
|
||||
let nextTwoHourDate = calendar.date(byAdding: .hour, value: 2, to: roundedToHourDate)
|
||||
let maxDate = calendar.date(byAdding: .day, value: 7, to: currentDate)
|
||||
|
||||
if let date = calendar.date(byAdding: .day, value: 365, to: currentDate) {
|
||||
self.pickerView?.maximumDate = date
|
||||
}
|
||||
|
||||
if let next1MinDate = next1MinDate, let next5MinDate = next5MinDate {
|
||||
if let next1MinDate = next1MinDate, let nextTwoHourDate = nextTwoHourDate {
|
||||
self.pickerView?.minimumDate = next1MinDate
|
||||
self.pickerView?.date = next5MinDate
|
||||
self.pickerView?.maximumDate = maxDate
|
||||
self.pickerView?.date = nextTwoHourDate
|
||||
}
|
||||
}
|
||||
|
||||
private func setupPickerView() {
|
||||
private func setupSchedulePickerView() {
|
||||
var currentDate: Date?
|
||||
if let pickerView = self.pickerView {
|
||||
currentDate = pickerView.date
|
||||
@ -2271,7 +2288,9 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
let calendar = Calendar(identifier: .gregorian)
|
||||
let time = stringForMessageTimestamp(timestamp: Int32(date.timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat)
|
||||
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
let timestamp = Int32(date.timeIntervalSince1970)
|
||||
let time = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: self.presentationData.dateTimeFormat)
|
||||
let buttonTitle: String
|
||||
if calendar.isDateInToday(date) {
|
||||
buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleToday(time).0
|
||||
@ -2282,6 +2301,15 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
self.scheduleButtonTitle = buttonTitle
|
||||
|
||||
let delta = timestamp - currentTimestamp
|
||||
|
||||
var isGroup = true
|
||||
if let peer = self.peer as? TelegramChannel, case .broadcast = peer.info {
|
||||
isGroup = false
|
||||
}
|
||||
let intervalString = timeIntervalString(strings: self.presentationData.strings, value: max(60, delta))
|
||||
self.scheduleTextNode.attributedText = NSAttributedString(string: isGroup ? self.presentationData.strings.ScheduleVoiceChat_GroupText(intervalString).0 : self.presentationData.strings.ScheduleVoiceChat_ChannelText(intervalString).0, font: Font.regular(14.0), textColor: UIColor(rgb: 0x8e8e93))
|
||||
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
@ -2341,16 +2369,22 @@ public final class VoiceChatController: ViewController {
|
||||
self.actionButton.titleLabel.layer.animatePosition(from: CGPoint(x: 0.0, y: -26.0), to: CGPoint(), duration: 0.2, additive: true)
|
||||
|
||||
if let pickerView = self.pickerView {
|
||||
self.pickerView = nil
|
||||
pickerView.alpha = 0.0
|
||||
pickerView.layer.animateScale(from: 1.0, to: 0.25, duration: 0.15, removeOnCompletion: false)
|
||||
pickerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
pickerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak pickerView] _ in
|
||||
pickerView?.removeFromSuperview()
|
||||
})
|
||||
pickerView.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
self.timerNode.alpha = 1.0
|
||||
self.timerNode.isHidden = false
|
||||
self.timerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
self.timerNode.animateIn()
|
||||
|
||||
self.scheduleTextNode.alpha = 0.0
|
||||
self.scheduleTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
|
||||
|
||||
self.updateTitle(slide: true, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
|
||||
@ -2362,7 +2396,9 @@ public final class VoiceChatController: ViewController {
|
||||
self.listNode.isUserInteractionEnabled = true
|
||||
|
||||
self.timerNode.alpha = 0.0
|
||||
self.timerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
self.timerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
|
||||
self?.timerNode.isHidden = true
|
||||
})
|
||||
|
||||
self.updateTitle(transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
@ -2602,7 +2638,6 @@ public final class VoiceChatController: ViewController {
|
||||
self.schedule()
|
||||
} else if callState.canManageCall {
|
||||
self.call.startScheduled()
|
||||
self.transitionToCall()
|
||||
} else {
|
||||
self.call.toggleScheduledSubscription(!callState.subscribedToScheduled)
|
||||
}
|
||||
@ -2678,8 +2713,13 @@ public final class VoiceChatController: ViewController {
|
||||
let _ = (self.inviteLinksPromise.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] inviteLinks in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let inviteLinks = inviteLinks {
|
||||
self?.presentShare(inviteLinks)
|
||||
strongSelf.presentShare(inviteLinks)
|
||||
} else if let addressName = strongSelf.peer?.addressName, !addressName.isEmpty {
|
||||
strongSelf.presentShare(GroupCallInviteLinks(listenerLink: "https://t.me/\(addressName)?voicechat", speakerLink: nil))
|
||||
}
|
||||
})
|
||||
return
|
||||
@ -2790,8 +2830,6 @@ public final class VoiceChatController: ViewController {
|
||||
} else {
|
||||
topInset = max(0.0, panInitialTopInset + min(0.0, panOffset))
|
||||
}
|
||||
} else if let _ = self.animation {
|
||||
topInset = self.listNode.frame.minY - listTopInset
|
||||
} else if let currentTopInset = self.topInset {
|
||||
topInset = self.isExpanded ? 0.0 : currentTopInset
|
||||
} else {
|
||||
@ -2845,7 +2883,7 @@ public final class VoiceChatController: ViewController {
|
||||
var bottomEdge: CGFloat = 0.0
|
||||
self.listNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ListViewItemNode {
|
||||
let convertedFrame = self.listNode.view.convert(itemNode.frame, to: self.view)
|
||||
let convertedFrame = self.listNode.view.convert(itemNode.frame, to: self.contentContainer.view)
|
||||
if convertedFrame.maxY > bottomEdge {
|
||||
bottomEdge = convertedFrame.maxY
|
||||
}
|
||||
@ -2853,14 +2891,7 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
let listMaxY = listTopInset + listSize.height
|
||||
if bottomEdge.isZero {
|
||||
bottomEdge = listMaxY
|
||||
}
|
||||
|
||||
var bottomOffset: CGFloat = 0.0
|
||||
if bottomEdge < listMaxY && (self.panGestureArguments != nil || self.isExpanded) {
|
||||
bottomOffset = bottomEdge - listMaxY
|
||||
}
|
||||
let bottomOffset: CGFloat = min(0.0, bottomEdge - listMaxY)
|
||||
|
||||
let bottomCornersFrame = CGRect(origin: CGPoint(x: sideInset, y: -50.0 + bottomOffset), size: CGSize(width: size.width - sideInset * 2.0, height: 50.0))
|
||||
let previousBottomCornersFrame = self.bottomCornersNode.frame
|
||||
@ -3139,9 +3170,7 @@ public final class VoiceChatController: ViewController {
|
||||
topInset = listSize.height
|
||||
}
|
||||
|
||||
if self.animation == nil {
|
||||
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset + topInset), size: listSize))
|
||||
}
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listSize, insets: insets, duration: duration, curve: curve)
|
||||
@ -3161,6 +3190,9 @@ public final class VoiceChatController: ViewController {
|
||||
transition.updateFrame(node: self.timerNode, frame: timerFrame)
|
||||
self.timerNode.update(size: timerFrame.size, scheduleTime: self.callState?.scheduleTimestamp, transition: .immediate)
|
||||
|
||||
let scheduleTextSize = self.scheduleTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
self.scheduleTextNode.frame = CGRect(origin: CGPoint(x: floor((size.width - scheduleTextSize.width) / 2.0), y: layout.size.height - layout.intrinsicInsets.bottom - scheduleTextSize.height - 145.0), size: scheduleTextSize)
|
||||
|
||||
let centralButtonSide = min(size.width, size.height) - 32.0
|
||||
let centralButtonSize = CGSize(width: centralButtonSide, height: centralButtonSide)
|
||||
let cameraButtonSize = CGSize(width: 36.0, height: 36.0)
|
||||
@ -3188,7 +3220,7 @@ public final class VoiceChatController: ViewController {
|
||||
smallButtons = false
|
||||
firstButtonFrame = CGRect(origin: CGPoint(x: floor(leftButtonFrame.midX - cameraButtonSize.width / 2.0), y: leftButtonFrame.minY - upperButtonDistance - cameraButtonSize.height), size: cameraButtonSize)
|
||||
secondButtonFrame = leftButtonFrame
|
||||
thirdButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - centralButtonSize.width) / 2.0), y: floorToScreenPixels((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0)), size: centralButtonSize)
|
||||
thirdButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - centralButtonSize.width) / 2.0), y: floor((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0) - 3.0), size: centralButtonSize)
|
||||
forthButtonFrame = rightButtonFrame
|
||||
case let .fullscreen(controlsHidden):
|
||||
smallButtons = true
|
||||
@ -3398,10 +3430,13 @@ public final class VoiceChatController: ViewController {
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
if self.callState?.scheduleTimestamp != nil && self.listNode.alpha > 0.0 {
|
||||
self.timerNode.isHidden = false
|
||||
self.listNode.alpha = 0.0
|
||||
self.listNode.isUserInteractionEnabled = false
|
||||
self.backgroundNode.backgroundColor = panelBackgroundColor
|
||||
self.updateIsFullscreen(false)
|
||||
} else if self.callState?.scheduleTimestamp == nil && !self.isScheduling && self.listNode.alpha == 0.0 {
|
||||
self.transitionToCall()
|
||||
}
|
||||
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
@ -3412,7 +3447,7 @@ public final class VoiceChatController: ViewController {
|
||||
if transition.crossFade {
|
||||
options.insert(.AnimateCrossfade)
|
||||
}
|
||||
if transition.animated && self.animation == nil {
|
||||
if transition.animated {
|
||||
options.insert(.AnimateInsertion)
|
||||
}
|
||||
}
|
||||
@ -3443,11 +3478,7 @@ public final class VoiceChatController: ViewController {
|
||||
let listTopInset = layoutTopInset + 63.0
|
||||
let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight)
|
||||
|
||||
if self.isScheduling || self.callState?.scheduleTimestamp != nil {
|
||||
self.topInset = listSize.height - 46.0 - floor(56.0 * 3.5)
|
||||
} else {
|
||||
self.topInset = max(0.0, max(listSize.height - itemsHeight, listSize.height - 46.0 - floor(56.0 * 3.5)))
|
||||
}
|
||||
|
||||
let targetY = listTopInset + (self.topInset ?? listSize.height)
|
||||
|
||||
@ -3455,24 +3486,20 @@ public final class VoiceChatController: ViewController {
|
||||
var frame = self.listNode.frame
|
||||
frame.origin.y = targetY
|
||||
self.listNode.frame = frame
|
||||
} else if !self.isExpanded {
|
||||
if self.listNode.frame.minY != targetY && !self.animatingExpansion && self.panGestureArguments == nil {
|
||||
self.animation = ListViewAnimation(from: self.listNode.frame.minY, to: targetY, duration: 0.4, curve: listViewAnimationCurveSystem, beginAt: CACurrentMediaTime(), update: { [weak self] _, currentValue in
|
||||
if let strongSelf = self {
|
||||
var frame = strongSelf.listNode.frame
|
||||
frame.origin.y = currentValue
|
||||
strongSelf.listNode.frame = frame
|
||||
strongSelf.updateFloatingHeaderOffset(offset: strongSelf.currentContentOffset ?? 0.0, transition: .immediate)
|
||||
}
|
||||
})
|
||||
self.updateAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if transition.animated {
|
||||
self.animatingInsertion = true
|
||||
}
|
||||
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: nil, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if strongSelf.animatingInsertion {
|
||||
strongSelf.updateFloatingHeaderOffset(offset: self?.currentContentOffset ?? 0.0, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
strongSelf.animatingInsertion = false
|
||||
}
|
||||
if !strongSelf.didSetContentsReady {
|
||||
strongSelf.didSetContentsReady = true
|
||||
strongSelf.controller?.contentsReady.set(true)
|
||||
@ -3480,39 +3507,6 @@ public final class VoiceChatController: ViewController {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private var animator: ConstantDisplayLinkAnimator?
|
||||
private var animation: ListViewAnimation?
|
||||
private func updateAnimation() {
|
||||
var animate = false
|
||||
let timestamp = CACurrentMediaTime()
|
||||
|
||||
if let animation = self.animation {
|
||||
animation.applyAt(timestamp)
|
||||
|
||||
if animation.completeAt(timestamp) {
|
||||
self.animation = nil
|
||||
} else {
|
||||
animate = true
|
||||
}
|
||||
}
|
||||
|
||||
if animate {
|
||||
let animator: ConstantDisplayLinkAnimator
|
||||
if let current = self.animator {
|
||||
animator = current
|
||||
} else {
|
||||
animator = ConstantDisplayLinkAnimator(update: { [weak self] in
|
||||
self?.updateAnimation()
|
||||
})
|
||||
self.animator = animator
|
||||
}
|
||||
animator.isPaused = false
|
||||
} else {
|
||||
self.animator?.isPaused = true
|
||||
}
|
||||
}
|
||||
|
||||
private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, callMembers: ([GroupCallParticipantsContext.Participant], String?), invitedPeers: [Peer], speakingPeers: Set<PeerId>) {
|
||||
var disableAnimation = false
|
||||
if self.currentCallMembers?.1 != callMembers.1 {
|
||||
@ -3689,6 +3683,8 @@ public final class VoiceChatController: ViewController {
|
||||
self.itemInteraction?.isExpanded = self.isExpanded
|
||||
}
|
||||
}
|
||||
|
||||
private var animatingInsertion = false
|
||||
private var animatingExpansion = false
|
||||
private var panGestureArguments: (topInset: CGFloat, offset: CGFloat)?
|
||||
|
||||
@ -3914,8 +3910,9 @@ public final class VoiceChatController: ViewController {
|
||||
return
|
||||
}
|
||||
|
||||
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_EditTitleTitle, text: strongSelf.presentationData.strings.VoiceChat_EditTitleText, placeholder: chatPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: strongSelf.callState?.title, maxLength: 40, apply: { title in
|
||||
if let strongSelf = self, let title = title {
|
||||
let initialTitle = strongSelf.callState?.title ?? ""
|
||||
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_EditTitleTitle, text: strongSelf.presentationData.strings.VoiceChat_EditTitleText, placeholder: chatPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { title in
|
||||
if let strongSelf = self, let title = title, title != initialTitle {
|
||||
strongSelf.call.updateTitle(title)
|
||||
|
||||
strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).0), action: { _ in return false })
|
||||
|
||||
@ -158,12 +158,12 @@ final class VoiceChatMicrophoneNode: ASDisplayNode {
|
||||
|
||||
context.setFillColor(parameters.color.cgColor)
|
||||
|
||||
var clearLineWidth: CGFloat = 4.0
|
||||
var clearLineWidth: CGFloat = 2.0
|
||||
var lineWidth: CGFloat = 1.0 + UIScreenPixel
|
||||
if bounds.size.width > 36.0 {
|
||||
context.scaleBy(x: 2.0, y: 2.0)
|
||||
} else if bounds.size.width < 30.0 {
|
||||
clearLineWidth = 3.0
|
||||
clearLineWidth = 2.0
|
||||
lineWidth = 1.0
|
||||
}
|
||||
|
||||
@ -207,18 +207,19 @@ final class VoiceChatMicrophoneNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
if parameters.reverse {
|
||||
startPoint = CGPoint(x: origin.x + length * (1.0 - parameters.transition), y: origin.y + length * (1.0 - parameters.transition))
|
||||
endPoint = CGPoint(x: origin.x + length, y: origin.y + length)
|
||||
startPoint = CGPoint(x: origin.x + length * (1.0 - parameters.transition), y: origin.y + length * (1.0 - parameters.transition)).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel)
|
||||
endPoint = CGPoint(x: origin.x + length, y: origin.y + length).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel)
|
||||
} else {
|
||||
startPoint = origin
|
||||
endPoint = CGPoint(x: origin.x + length * parameters.transition, y: origin.y + length * parameters.transition)
|
||||
startPoint = origin.offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel)
|
||||
endPoint = CGPoint(x: origin.x + length * parameters.transition, y: origin.y + length * parameters.transition).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel)
|
||||
}
|
||||
|
||||
|
||||
context.setBlendMode(.clear)
|
||||
context.setLineWidth(clearLineWidth)
|
||||
|
||||
context.move(to: startPoint)
|
||||
context.addLine(to: endPoint)
|
||||
context.move(to: startPoint.offsetBy(dx: 0.0, dy: 1.0 + UIScreenPixel))
|
||||
context.addLine(to: endPoint.offsetBy(dx: 0.0, dy: 1.0 + UIScreenPixel))
|
||||
context.strokePath()
|
||||
|
||||
context.setBlendMode(.normal)
|
||||
|
||||
@ -169,7 +169,7 @@ public final class VoiceChatOverlayController: ViewController {
|
||||
|
||||
if reclaim {
|
||||
self.dismissed = true
|
||||
let targetPosition = CGPoint(x: layout.size.width / 2.0, y: layout.size.height - layout.intrinsicInsets.bottom - 205.0 / 2.0)
|
||||
let targetPosition = CGPoint(x: layout.size.width / 2.0, y: layout.size.height - layout.intrinsicInsets.bottom - 205.0 / 2.0 - 2.0)
|
||||
if self.isSlidOffscreen {
|
||||
self.isSlidOffscreen = false
|
||||
self.isButtonHidden = true
|
||||
|
||||
@ -133,10 +133,8 @@ final class VoiceChatTimerNode: ASDisplayNode {
|
||||
let timerText: String
|
||||
if elapsedTime >= 86400 {
|
||||
timerText = timeIntervalString(strings: self.strings, value: elapsedTime)
|
||||
} else if elapsedTime < 0 {
|
||||
timerText = "\(textForTimeout(value: abs(elapsedTime)))"
|
||||
} else {
|
||||
timerText = textForTimeout(value: elapsedTime)
|
||||
timerText = textForTimeout(value: abs(elapsedTime))
|
||||
}
|
||||
|
||||
if self.updateTimer == nil {
|
||||
|
||||
@ -551,6 +551,10 @@ private final class VoiceChatUserNameEditAlertContentNode: AlertContentNode {
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
|
||||
self.firstNameInputFieldNode.complete = { [weak self] in
|
||||
self?.lastNameInputFieldNode.activateInput()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
||||
@ -393,11 +393,12 @@ public func sendBotPaymentForm(account: Account, messageId: MessageId, formId: I
|
||||
}
|
||||
}
|
||||
|
||||
public struct BotPaymentReceipt : Equatable {
|
||||
public struct BotPaymentReceipt {
|
||||
public let invoice: BotPaymentInvoice
|
||||
public let info: BotPaymentRequestedInfo?
|
||||
public let shippingOption: BotPaymentShippingOption?
|
||||
public let credentialsTitle: String
|
||||
public let invoiceMedia: TelegramMediaInvoice
|
||||
public let tipAmount: Int64?
|
||||
}
|
||||
|
||||
@ -419,15 +420,56 @@ public func requestBotPaymentReceipt(account: Account, messageId: MessageId) ->
|
||||
|> mapError { _ -> RequestBotPaymentReceiptError in
|
||||
return .generic
|
||||
}
|
||||
|> map { result -> BotPaymentReceipt in
|
||||
|> mapToSignal { result -> Signal<BotPaymentReceipt, RequestBotPaymentReceiptError> in
|
||||
return account.postbox.transaction { transaction -> BotPaymentReceipt in
|
||||
switch result {
|
||||
case let .paymentReceipt(flags, date, botId, providerId, title, description, photo, invoice, info, shipping, tipAmount, currency, totalAmount, credentialsTitle, users):
|
||||
var peers: [Peer] = []
|
||||
for user in users {
|
||||
peers.append(TelegramUser(user: user))
|
||||
}
|
||||
updatePeers(transaction: transaction, peers: peers, update: { _, updated in return updated })
|
||||
|
||||
let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice)
|
||||
let parsedInfo = info.flatMap(BotPaymentRequestedInfo.init)
|
||||
let shippingOption = shipping.flatMap(BotPaymentShippingOption.init)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -184,6 +184,7 @@ public func getCurrentGroupCall(account: Account, callId: Int64, accessHash: Int
|
||||
public enum CreateGroupCallError {
|
||||
case generic
|
||||
case anonymousNotAllowed
|
||||
case scheduledTooLate
|
||||
}
|
||||
|
||||
public func createGroupCall(account: Account, peerId: PeerId, title: String?, scheduleDate: Int32?) -> Signal<GroupCallInfo, CreateGroupCallError> {
|
||||
@ -208,6 +209,8 @@ public func createGroupCall(account: Account, peerId: PeerId, title: String?, sc
|
||||
|> mapError { error -> CreateGroupCallError in
|
||||
if error.errorDescription == "ANONYMOUS_CALLS_DISABLED" {
|
||||
return .anonymousNotAllowed
|
||||
} else if error.errorDescription == "SCHEDULE_DATE_TOO_LATE" {
|
||||
return .scheduledTooLate
|
||||
}
|
||||
return .generic
|
||||
}
|
||||
@ -1005,6 +1008,11 @@ public final class GroupCallParticipantsContext {
|
||||
public struct DefaultParticipantsAreMuted: Equatable {
|
||||
public var isMuted: Bool
|
||||
public var canChange: Bool
|
||||
|
||||
public init(isMuted: Bool, canChange: Bool) {
|
||||
self.isMuted = isMuted
|
||||
self.canChange = canChange
|
||||
}
|
||||
}
|
||||
|
||||
public var participants: [Participant]
|
||||
@ -1037,6 +1045,34 @@ public final class GroupCallParticipantsContext {
|
||||
|
||||
self.participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: self.sortAscending) })
|
||||
}
|
||||
|
||||
public init(
|
||||
participants: [Participant],
|
||||
nextParticipantsFetchOffset: String?,
|
||||
adminIds: Set<PeerId>,
|
||||
isCreator: Bool,
|
||||
defaultParticipantsAreMuted: DefaultParticipantsAreMuted,
|
||||
sortAscending: Bool,
|
||||
recordingStartTimestamp: Int32?,
|
||||
title: String?,
|
||||
scheduleTimestamp: Int32?,
|
||||
subscribedToScheduled: Bool,
|
||||
totalCount: Int,
|
||||
version: Int32
|
||||
) {
|
||||
self.participants = participants
|
||||
self.nextParticipantsFetchOffset = nextParticipantsFetchOffset
|
||||
self.adminIds = adminIds
|
||||
self.isCreator = isCreator
|
||||
self.defaultParticipantsAreMuted = defaultParticipantsAreMuted
|
||||
self.sortAscending = sortAscending
|
||||
self.recordingStartTimestamp = recordingStartTimestamp
|
||||
self.title = title
|
||||
self.scheduleTimestamp = scheduleTimestamp
|
||||
self.subscribedToScheduled = subscribedToScheduled
|
||||
self.totalCount = totalCount
|
||||
self.version = version
|
||||
}
|
||||
}
|
||||
|
||||
private struct OverlayState: Equatable {
|
||||
|
||||
@ -359,7 +359,13 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
|
||||
pageIndicatorInactiveColor: UIColor(white: 1.0, alpha: 0.3),
|
||||
inputClearButtonColor: UIColor(rgb: 0x8b9197),
|
||||
itemBarChart: PresentationThemeItemBarChart(color1: UIColor(rgb: 0xffffff), color2: UIColor(rgb: 0x929196), color3: UIColor(rgb: 0x333333)),
|
||||
itemInputField: PresentationInputFieldTheme(backgroundColor: UIColor(rgb: 0x0f0f0f), strokeColor: UIColor(rgb: 0x0f0f0f), placeholderColor: UIColor(rgb: 0x8f8f8f), primaryColor: UIColor(rgb: 0xffffff), controlColor: UIColor(rgb: 0x8f8f8f))
|
||||
itemInputField: PresentationInputFieldTheme(backgroundColor: UIColor(rgb: 0x0f0f0f), strokeColor: UIColor(rgb: 0x0f0f0f), placeholderColor: UIColor(rgb: 0x8f8f8f), primaryColor: UIColor(rgb: 0xffffff), controlColor: UIColor(rgb: 0x8f8f8f)),
|
||||
paymentOption: PresentationThemeList.PaymentOption(
|
||||
inactiveFillColor: UIColor(rgb: 0x00A650).withMultipliedAlpha(0.3),
|
||||
inactiveForegroundColor: UIColor(rgb: 0x00A650),
|
||||
activeFillColor: UIColor(rgb: 0x00A650),
|
||||
activeForegroundColor: UIColor(rgb: 0xffffff)
|
||||
)
|
||||
)
|
||||
|
||||
let chatList = PresentationThemeChatList(
|
||||
|
||||
@ -612,7 +612,13 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres
|
||||
pageIndicatorInactiveColor: mainSecondaryTextColor.withAlphaComponent(0.4),
|
||||
inputClearButtonColor: mainSecondaryColor,
|
||||
itemBarChart: PresentationThemeItemBarChart(color1: accentColor, color2: mainSecondaryTextColor.withAlphaComponent(0.5), color3: accentColor.withMultiplied(hue: 1.038, saturation: 0.329, brightness: 0.33)),
|
||||
itemInputField: PresentationInputFieldTheme(backgroundColor: mainInputColor, strokeColor: mainInputColor, placeholderColor: mainSecondaryColor, primaryColor: UIColor(rgb: 0xffffff), controlColor: mainSecondaryColor)
|
||||
itemInputField: PresentationInputFieldTheme(backgroundColor: mainInputColor, strokeColor: mainInputColor, placeholderColor: mainSecondaryColor, primaryColor: UIColor(rgb: 0xffffff), controlColor: mainSecondaryColor),
|
||||
paymentOption: PresentationThemeList.PaymentOption(
|
||||
inactiveFillColor: UIColor(rgb: 0x00A650).withMultipliedAlpha(0.3),
|
||||
inactiveForegroundColor: UIColor(rgb: 0x00A650),
|
||||
activeFillColor: UIColor(rgb: 0x00A650),
|
||||
activeForegroundColor: UIColor(rgb: 0xffffff)
|
||||
)
|
||||
)
|
||||
|
||||
let chatList = PresentationThemeChatList(
|
||||
|
||||
@ -448,7 +448,13 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
pageIndicatorInactiveColor: UIColor(rgb: 0xe3e3e7),
|
||||
inputClearButtonColor: UIColor(rgb: 0xcccccc),
|
||||
itemBarChart: PresentationThemeItemBarChart(color1: UIColor(rgb: 0x007ee5), color2: UIColor(rgb: 0xc8c7cc), color3: UIColor(rgb: 0xf2f1f7)),
|
||||
itemInputField: PresentationInputFieldTheme(backgroundColor: UIColor(rgb: 0xf2f2f7), strokeColor: UIColor(rgb: 0xf2f2f7), placeholderColor: UIColor(rgb: 0xb6b6bb), primaryColor: UIColor(rgb: 0x000000), controlColor: UIColor(rgb: 0xb6b6bb))
|
||||
itemInputField: PresentationInputFieldTheme(backgroundColor: UIColor(rgb: 0xf2f2f7), strokeColor: UIColor(rgb: 0xf2f2f7), placeholderColor: UIColor(rgb: 0xb6b6bb), primaryColor: UIColor(rgb: 0x000000), controlColor: UIColor(rgb: 0xb6b6bb)),
|
||||
paymentOption: PresentationThemeList.PaymentOption(
|
||||
inactiveFillColor: UIColor(rgb: 0x00A650).withMultipliedAlpha(0.1),
|
||||
inactiveForegroundColor: UIColor(rgb: 0x00A650),
|
||||
activeFillColor: UIColor(rgb: 0x00A650),
|
||||
activeForegroundColor: UIColor(rgb: 0xffffff)
|
||||
)
|
||||
)
|
||||
|
||||
let chatList = PresentationThemeChatList(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -406,6 +406,25 @@ public final class PresentationInputFieldTheme {
|
||||
}
|
||||
|
||||
public final class PresentationThemeList {
|
||||
public final class PaymentOption {
|
||||
public let inactiveFillColor: UIColor
|
||||
public let inactiveForegroundColor: UIColor
|
||||
public let activeFillColor: UIColor
|
||||
public let activeForegroundColor: UIColor
|
||||
|
||||
public init(
|
||||
inactiveFillColor: UIColor,
|
||||
inactiveForegroundColor: UIColor,
|
||||
activeFillColor: UIColor,
|
||||
activeForegroundColor: UIColor
|
||||
) {
|
||||
self.inactiveFillColor = inactiveFillColor
|
||||
self.inactiveForegroundColor = inactiveForegroundColor
|
||||
self.activeFillColor = activeFillColor
|
||||
self.activeForegroundColor = activeForegroundColor
|
||||
}
|
||||
}
|
||||
|
||||
public let blocksBackgroundColor: UIColor
|
||||
public let plainBackgroundColor: UIColor
|
||||
public let itemPrimaryTextColor: UIColor
|
||||
@ -437,8 +456,42 @@ public final class PresentationThemeList {
|
||||
public let inputClearButtonColor: UIColor
|
||||
public let itemBarChart: PresentationThemeItemBarChart
|
||||
public let itemInputField: PresentationInputFieldTheme
|
||||
public let paymentOption: PaymentOption
|
||||
|
||||
public init(blocksBackgroundColor: UIColor, plainBackgroundColor: UIColor, itemPrimaryTextColor: UIColor, itemSecondaryTextColor: UIColor, itemDisabledTextColor: UIColor, itemAccentColor: UIColor, itemHighlightedColor: UIColor, itemDestructiveColor: UIColor, itemPlaceholderTextColor: UIColor, itemBlocksBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, itemBlocksSeparatorColor: UIColor, itemPlainSeparatorColor: UIColor, disclosureArrowColor: UIColor, sectionHeaderTextColor: UIColor, freeTextColor: UIColor, freeTextErrorColor: UIColor, freeTextSuccessColor: UIColor, freeMonoIconColor: UIColor, itemSwitchColors: PresentationThemeSwitch, itemDisclosureActions: PresentationThemeItemDisclosureActions, itemCheckColors: PresentationThemeFillStrokeForeground, controlSecondaryColor: UIColor, freeInputField: PresentationInputFieldTheme, freePlainInputField: PresentationInputFieldTheme, mediaPlaceholderColor: UIColor, scrollIndicatorColor: UIColor, pageIndicatorInactiveColor: UIColor, inputClearButtonColor: UIColor, itemBarChart: PresentationThemeItemBarChart, itemInputField: PresentationInputFieldTheme) {
|
||||
public init(
|
||||
blocksBackgroundColor: UIColor,
|
||||
plainBackgroundColor: UIColor,
|
||||
itemPrimaryTextColor: UIColor,
|
||||
itemSecondaryTextColor: UIColor,
|
||||
itemDisabledTextColor: UIColor,
|
||||
itemAccentColor: UIColor,
|
||||
itemHighlightedColor: UIColor,
|
||||
itemDestructiveColor: UIColor,
|
||||
itemPlaceholderTextColor: UIColor,
|
||||
itemBlocksBackgroundColor: UIColor,
|
||||
itemHighlightedBackgroundColor: UIColor,
|
||||
itemBlocksSeparatorColor: UIColor,
|
||||
itemPlainSeparatorColor: UIColor,
|
||||
disclosureArrowColor: UIColor,
|
||||
sectionHeaderTextColor: UIColor,
|
||||
freeTextColor: UIColor,
|
||||
freeTextErrorColor: UIColor,
|
||||
freeTextSuccessColor: UIColor,
|
||||
freeMonoIconColor: UIColor,
|
||||
itemSwitchColors: PresentationThemeSwitch,
|
||||
itemDisclosureActions: PresentationThemeItemDisclosureActions,
|
||||
itemCheckColors: PresentationThemeFillStrokeForeground,
|
||||
controlSecondaryColor: UIColor,
|
||||
freeInputField: PresentationInputFieldTheme,
|
||||
freePlainInputField: PresentationInputFieldTheme,
|
||||
mediaPlaceholderColor: UIColor,
|
||||
scrollIndicatorColor: UIColor,
|
||||
pageIndicatorInactiveColor: UIColor,
|
||||
inputClearButtonColor: UIColor,
|
||||
itemBarChart: PresentationThemeItemBarChart,
|
||||
itemInputField: PresentationInputFieldTheme,
|
||||
paymentOption: PaymentOption
|
||||
) {
|
||||
self.blocksBackgroundColor = blocksBackgroundColor
|
||||
self.plainBackgroundColor = plainBackgroundColor
|
||||
self.itemPrimaryTextColor = itemPrimaryTextColor
|
||||
@ -470,10 +523,11 @@ public final class PresentationThemeList {
|
||||
self.inputClearButtonColor = inputClearButtonColor
|
||||
self.itemBarChart = itemBarChart
|
||||
self.itemInputField = itemInputField
|
||||
self.paymentOption = paymentOption
|
||||
}
|
||||
|
||||
public func withUpdated(blocksBackgroundColor: UIColor? = nil, plainBackgroundColor: UIColor? = nil, itemPrimaryTextColor: UIColor? = nil, itemSecondaryTextColor: UIColor? = nil, itemDisabledTextColor: UIColor? = nil, itemAccentColor: UIColor? = nil, itemHighlightedColor: UIColor? = nil, itemDestructiveColor: UIColor? = nil, itemPlaceholderTextColor: UIColor? = nil, itemBlocksBackgroundColor: UIColor? = nil, itemHighlightedBackgroundColor: UIColor? = nil, itemBlocksSeparatorColor: UIColor? = nil, itemPlainSeparatorColor: UIColor? = nil, disclosureArrowColor: UIColor? = nil, sectionHeaderTextColor: UIColor? = nil, freeTextColor: UIColor? = nil, freeTextErrorColor: UIColor? = nil, freeTextSuccessColor: UIColor? = nil, freeMonoIconColor: UIColor? = nil, itemSwitchColors: PresentationThemeSwitch? = nil, itemDisclosureActions: PresentationThemeItemDisclosureActions? = nil, itemCheckColors: PresentationThemeFillStrokeForeground? = nil, controlSecondaryColor: UIColor? = nil, freeInputField: PresentationInputFieldTheme? = nil, freePlainInputField: PresentationInputFieldTheme? = nil, mediaPlaceholderColor: UIColor? = nil, scrollIndicatorColor: UIColor? = nil, pageIndicatorInactiveColor: UIColor? = nil, inputClearButtonColor: UIColor? = nil, itemBarChart: PresentationThemeItemBarChart? = nil, itemInputField: PresentationInputFieldTheme? = nil) -> PresentationThemeList {
|
||||
return PresentationThemeList(blocksBackgroundColor: blocksBackgroundColor ?? self.blocksBackgroundColor, plainBackgroundColor: plainBackgroundColor ?? self.plainBackgroundColor, itemPrimaryTextColor: itemPrimaryTextColor ?? self.itemPrimaryTextColor, itemSecondaryTextColor: itemSecondaryTextColor ?? self.itemSecondaryTextColor, itemDisabledTextColor: itemDisabledTextColor ?? self.itemDisabledTextColor, itemAccentColor: itemAccentColor ?? self.itemAccentColor, itemHighlightedColor: itemHighlightedColor ?? self.itemHighlightedColor, itemDestructiveColor: itemDestructiveColor ?? self.itemDestructiveColor, itemPlaceholderTextColor: itemPlaceholderTextColor ?? self.itemPlaceholderTextColor, itemBlocksBackgroundColor: itemBlocksBackgroundColor ?? self.itemBlocksBackgroundColor, itemHighlightedBackgroundColor: itemHighlightedBackgroundColor ?? self.itemHighlightedBackgroundColor, itemBlocksSeparatorColor: itemBlocksSeparatorColor ?? self.itemBlocksSeparatorColor, itemPlainSeparatorColor: itemPlainSeparatorColor ?? self.itemPlainSeparatorColor, disclosureArrowColor: disclosureArrowColor ?? self.disclosureArrowColor, sectionHeaderTextColor: sectionHeaderTextColor ?? self.sectionHeaderTextColor, freeTextColor: freeTextColor ?? self.freeTextColor, freeTextErrorColor: freeTextErrorColor ?? self.freeTextErrorColor, freeTextSuccessColor: freeTextSuccessColor ?? self.freeTextSuccessColor, freeMonoIconColor: freeMonoIconColor ?? self.freeMonoIconColor, itemSwitchColors: itemSwitchColors ?? self.itemSwitchColors, itemDisclosureActions: itemDisclosureActions ?? self.itemDisclosureActions, itemCheckColors: itemCheckColors ?? self.itemCheckColors, controlSecondaryColor: controlSecondaryColor ?? self.controlSecondaryColor, freeInputField: freeInputField ?? self.freeInputField, freePlainInputField: freePlainInputField ?? self.freePlainInputField, mediaPlaceholderColor: mediaPlaceholderColor ?? self.mediaPlaceholderColor, scrollIndicatorColor: scrollIndicatorColor ?? self.scrollIndicatorColor, pageIndicatorInactiveColor: pageIndicatorInactiveColor ?? self.pageIndicatorInactiveColor, inputClearButtonColor: inputClearButtonColor ?? self.inputClearButtonColor, itemBarChart: itemBarChart ?? self.itemBarChart, itemInputField: itemInputField ?? self.itemInputField)
|
||||
public func withUpdated(blocksBackgroundColor: UIColor? = nil, plainBackgroundColor: UIColor? = nil, itemPrimaryTextColor: UIColor? = nil, itemSecondaryTextColor: UIColor? = nil, itemDisabledTextColor: UIColor? = nil, itemAccentColor: UIColor? = nil, itemHighlightedColor: UIColor? = nil, itemDestructiveColor: UIColor? = nil, itemPlaceholderTextColor: UIColor? = nil, itemBlocksBackgroundColor: UIColor? = nil, itemHighlightedBackgroundColor: UIColor? = nil, itemBlocksSeparatorColor: UIColor? = nil, itemPlainSeparatorColor: UIColor? = nil, disclosureArrowColor: UIColor? = nil, sectionHeaderTextColor: UIColor? = nil, freeTextColor: UIColor? = nil, freeTextErrorColor: UIColor? = nil, freeTextSuccessColor: UIColor? = nil, freeMonoIconColor: UIColor? = nil, itemSwitchColors: PresentationThemeSwitch? = nil, itemDisclosureActions: PresentationThemeItemDisclosureActions? = nil, itemCheckColors: PresentationThemeFillStrokeForeground? = nil, controlSecondaryColor: UIColor? = nil, freeInputField: PresentationInputFieldTheme? = nil, freePlainInputField: PresentationInputFieldTheme? = nil, mediaPlaceholderColor: UIColor? = nil, scrollIndicatorColor: UIColor? = nil, pageIndicatorInactiveColor: UIColor? = nil, inputClearButtonColor: UIColor? = nil, itemBarChart: PresentationThemeItemBarChart? = nil, itemInputField: PresentationInputFieldTheme? = nil, paymentOption: PaymentOption? = nil) -> PresentationThemeList {
|
||||
return PresentationThemeList(blocksBackgroundColor: blocksBackgroundColor ?? self.blocksBackgroundColor, plainBackgroundColor: plainBackgroundColor ?? self.plainBackgroundColor, itemPrimaryTextColor: itemPrimaryTextColor ?? self.itemPrimaryTextColor, itemSecondaryTextColor: itemSecondaryTextColor ?? self.itemSecondaryTextColor, itemDisabledTextColor: itemDisabledTextColor ?? self.itemDisabledTextColor, itemAccentColor: itemAccentColor ?? self.itemAccentColor, itemHighlightedColor: itemHighlightedColor ?? self.itemHighlightedColor, itemDestructiveColor: itemDestructiveColor ?? self.itemDestructiveColor, itemPlaceholderTextColor: itemPlaceholderTextColor ?? self.itemPlaceholderTextColor, itemBlocksBackgroundColor: itemBlocksBackgroundColor ?? self.itemBlocksBackgroundColor, itemHighlightedBackgroundColor: itemHighlightedBackgroundColor ?? self.itemHighlightedBackgroundColor, itemBlocksSeparatorColor: itemBlocksSeparatorColor ?? self.itemBlocksSeparatorColor, itemPlainSeparatorColor: itemPlainSeparatorColor ?? self.itemPlainSeparatorColor, disclosureArrowColor: disclosureArrowColor ?? self.disclosureArrowColor, sectionHeaderTextColor: sectionHeaderTextColor ?? self.sectionHeaderTextColor, freeTextColor: freeTextColor ?? self.freeTextColor, freeTextErrorColor: freeTextErrorColor ?? self.freeTextErrorColor, freeTextSuccessColor: freeTextSuccessColor ?? self.freeTextSuccessColor, freeMonoIconColor: freeMonoIconColor ?? self.freeMonoIconColor, itemSwitchColors: itemSwitchColors ?? self.itemSwitchColors, itemDisclosureActions: itemDisclosureActions ?? self.itemDisclosureActions, itemCheckColors: itemCheckColors ?? self.itemCheckColors, controlSecondaryColor: controlSecondaryColor ?? self.controlSecondaryColor, freeInputField: freeInputField ?? self.freeInputField, freePlainInputField: freePlainInputField ?? self.freePlainInputField, mediaPlaceholderColor: mediaPlaceholderColor ?? self.mediaPlaceholderColor, scrollIndicatorColor: scrollIndicatorColor ?? self.scrollIndicatorColor, pageIndicatorInactiveColor: pageIndicatorInactiveColor ?? self.pageIndicatorInactiveColor, inputClearButtonColor: inputClearButtonColor ?? self.inputClearButtonColor, itemBarChart: itemBarChart ?? self.itemBarChart, itemInputField: itemInputField ?? self.itemInputField, paymentOption: paymentOption ?? self.paymentOption)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -745,6 +745,33 @@ extension PresentationInputFieldTheme: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
extension PresentationThemeList.PaymentOption: Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case inactiveFill
|
||||
case inactiveForeground
|
||||
case activeFill
|
||||
case activeForeground
|
||||
}
|
||||
|
||||
public convenience init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.init(
|
||||
inactiveFillColor: try decodeColor(values, .inactiveFill),
|
||||
inactiveForegroundColor: try decodeColor(values, .inactiveForeground),
|
||||
activeFillColor: try decodeColor(values, .activeFill),
|
||||
activeForegroundColor: try decodeColor(values, .activeForeground)
|
||||
)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var values = encoder.container(keyedBy: CodingKeys.self)
|
||||
try encodeColor(&values, self.activeFillColor, .inactiveFill)
|
||||
try encodeColor(&values, self.activeForegroundColor, .inactiveForeground)
|
||||
try encodeColor(&values, self.activeFillColor, .activeFill)
|
||||
try encodeColor(&values, self.activeForegroundColor, .activeForeground)
|
||||
}
|
||||
}
|
||||
|
||||
extension PresentationThemeList: Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case blocksBg
|
||||
@ -778,6 +805,7 @@ extension PresentationThemeList: Codable {
|
||||
case inputClearButton
|
||||
case itemBarChart
|
||||
case itemInputField
|
||||
case paymentOption
|
||||
}
|
||||
|
||||
public convenience init(from decoder: Decoder) throws {
|
||||
@ -790,6 +818,8 @@ extension PresentationThemeList: Codable {
|
||||
freePlainInputField = try values.decode(PresentationInputFieldTheme.self, forKey: .freeInputField)
|
||||
}
|
||||
|
||||
let freeTextSuccessColor = try decodeColor(values, .freeTextSuccess)
|
||||
|
||||
self.init(
|
||||
blocksBackgroundColor: try decodeColor(values, .blocksBg),
|
||||
plainBackgroundColor: try decodeColor(values, .plainBg),
|
||||
@ -808,7 +838,7 @@ extension PresentationThemeList: Codable {
|
||||
sectionHeaderTextColor: try decodeColor(values, .sectionHeaderText),
|
||||
freeTextColor: try decodeColor(values, .freeText),
|
||||
freeTextErrorColor: try decodeColor(values, .freeTextError),
|
||||
freeTextSuccessColor: try decodeColor(values, .freeTextSuccess),
|
||||
freeTextSuccessColor: freeTextSuccessColor,
|
||||
freeMonoIconColor: try decodeColor(values, .freeMonoIcon),
|
||||
itemSwitchColors: try values.decode(PresentationThemeSwitch.self, forKey: .switch),
|
||||
itemDisclosureActions: try values.decode(PresentationThemeItemDisclosureActions.self, forKey: .disclosureActions),
|
||||
@ -821,7 +851,13 @@ extension PresentationThemeList: Codable {
|
||||
pageIndicatorInactiveColor: try decodeColor(values, .pageIndicatorInactive),
|
||||
inputClearButtonColor: try decodeColor(values, .inputClearButton),
|
||||
itemBarChart: try values.decode(PresentationThemeItemBarChart.self, forKey: .itemBarChart),
|
||||
itemInputField: try values.decode(PresentationInputFieldTheme.self, forKey: .itemInputField)
|
||||
itemInputField: try values.decode(PresentationInputFieldTheme.self, forKey: .itemInputField),
|
||||
paymentOption: (try? values.decode(PresentationThemeList.PaymentOption.self, forKey: .paymentOption)) ?? PresentationThemeList.PaymentOption(
|
||||
inactiveFillColor: freeTextSuccessColor.withMultipliedAlpha(0.3),
|
||||
inactiveForegroundColor: freeTextSuccessColor,
|
||||
activeFillColor: freeTextSuccessColor,
|
||||
activeForegroundColor: UIColor(rgb: 0xffffff)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -165,7 +165,7 @@ public func formatCurrencyAmount(_ amount: Int64, currency: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
public func formatCurrencyAmountCustom(_ amount: Int64, currency: String) -> (String, String) {
|
||||
public func formatCurrencyAmountCustom(_ amount: Int64, currency: String) -> (String, String, Bool) {
|
||||
if let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] {
|
||||
var result = ""
|
||||
if amount < 0 {
|
||||
@ -198,8 +198,8 @@ public func formatCurrencyAmountCustom(_ amount: Int64, currency: String) -> (St
|
||||
result.append(entry.symbol)
|
||||
}*/
|
||||
|
||||
return (result, entry.symbol)
|
||||
return (result, entry.symbol, entry.symbolOnLeft)
|
||||
} else {
|
||||
return ("", "")
|
||||
return ("", "", false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -449,8 +449,14 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
case let .groupPhoneCall(_, _, scheduleDate, duration):
|
||||
if let scheduleDate = scheduleDate {
|
||||
let timeString = humanReadableStringForTimestamp(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: scheduleDate)
|
||||
let titleString = strings.Notification_VoiceChatScheduled(timeString).0
|
||||
if message.author?.id.namespace == Namespaces.Peer.CloudChannel {
|
||||
let titleString = strings.Notification_VoiceChatScheduledChannel(timeString).0
|
||||
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
|
||||
} else {
|
||||
let attributePeerIds: [(Int, PeerId?)] = [(0, message.author?.id)]
|
||||
let titleString = strings.Notification_VoiceChatScheduled(authorName, timeString)
|
||||
attributedString = addAttributesToStringWithRanges(titleString, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: attributePeerIds))
|
||||
}
|
||||
} else if let duration = duration {
|
||||
let titleString = strings.Notification_VoiceChatEnded(callDurationString(strings: strings, value: duration)).0
|
||||
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
|
||||
|
||||
Binary file not shown.
@ -815,14 +815,14 @@ final class SharedApplicationContext {
|
||||
}
|
||||
})
|
||||
|
||||
self.mainWindow.debugAction = {
|
||||
/*self.mainWindow.debugAction = {
|
||||
self.mainWindow.debugAction = nil
|
||||
|
||||
let presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme))
|
||||
navigationController.viewControllers = [debugController(sharedContext: sharedContext, context: nil)]
|
||||
self.mainWindow.present(navigationController, on: .root)
|
||||
}
|
||||
}*/
|
||||
|
||||
presentationDataPromise.set(sharedContext.presentationData)
|
||||
|
||||
|
||||
@ -357,6 +357,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
private weak var sendMessageActionsController: ChatSendMessageActionSheetController?
|
||||
private var searchResultsController: ChatSearchResultsController?
|
||||
|
||||
private weak var currentPinchController: PinchController?
|
||||
private weak var currentPinchSourceItemNode: ListViewItemNode?
|
||||
|
||||
private var screenCaptureManager: ScreenCaptureDetectionManager?
|
||||
private let chatAdditionalDataDisposable = MetaDisposable()
|
||||
|
||||
@ -579,7 +582,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
let text: String
|
||||
switch error {
|
||||
case .generic:
|
||||
case .generic, .scheduledTooLate:
|
||||
text = strongSelf.presentationData.strings.Login_UnknownError
|
||||
case .anonymousNotAllowed:
|
||||
text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText
|
||||
@ -624,12 +627,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
strongSelf.presentAutoremoveSetup()
|
||||
}
|
||||
case .paymentSent:
|
||||
for attribute in message.attributes {
|
||||
strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: message.id), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
/*for attribute in message.attributes {
|
||||
if let attribute = attribute as? ReplyMessageAttribute {
|
||||
strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId))
|
||||
//strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId))
|
||||
break
|
||||
}
|
||||
}
|
||||
}*/
|
||||
return true
|
||||
default:
|
||||
break
|
||||
@ -920,7 +924,26 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let pinchController = PinchController(sourceNode: sourceNode)
|
||||
|
||||
var sourceItemNode: ListViewItemNode?
|
||||
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||||
guard let itemNode = itemNode as? ListViewItemNode else {
|
||||
return
|
||||
}
|
||||
if sourceNode.view.isDescendant(of: itemNode.view) {
|
||||
sourceItemNode = itemNode
|
||||
}
|
||||
}
|
||||
|
||||
let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: {
|
||||
guard let strongSelf = self else {
|
||||
return CGRect()
|
||||
}
|
||||
|
||||
return strongSelf.chatDisplayNode.view.convert(strongSelf.chatDisplayNode.frameForVisibleArea(), to: nil)
|
||||
})
|
||||
strongSelf.currentPinchController = pinchController
|
||||
strongSelf.currentPinchSourceItemNode = sourceItemNode
|
||||
strongSelf.window?.presentInGlobalOverlay(pinchController)
|
||||
}, openMessageContextActions: { message, node, rect, gesture in
|
||||
gesture?.cancel()
|
||||
@ -1848,7 +1871,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if let invoice = media as? TelegramMediaInvoice {
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
if let receiptMessageId = invoice.receiptMessageId {
|
||||
strongSelf.present(BotReceiptController(context: strongSelf.context, invoice: invoice, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
} else {
|
||||
strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, messageId: messageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}
|
||||
@ -4011,21 +4034,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
for (tooltipScreen, tooltipItemNode) in strongSelf.currentMessageTooltipScreens {
|
||||
if let itemNode = itemNode {
|
||||
if itemNode === tooltipItemNode {
|
||||
tooltipScreen.addRelativeScrollingOffset(-offset, transition: transition)
|
||||
}
|
||||
} else {
|
||||
tooltipScreen.addRelativeScrollingOffset(-offset, transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.chatDisplayNode.historyNode.didScrollWithOffset = { [weak self] offset, _, _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if offset > 0.0 {
|
||||
if var scrolledToMessageIdValue = strongSelf.scrolledToMessageIdValue {
|
||||
@ -4035,6 +4043,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
} else if offset < 0.0 {
|
||||
strongSelf.scrolledToMessageIdValue = nil
|
||||
}
|
||||
|
||||
if let currentPinchSourceItemNode = strongSelf.currentPinchSourceItemNode {
|
||||
if let itemNode = itemNode {
|
||||
if itemNode === currentPinchSourceItemNode {
|
||||
strongSelf.currentPinchController?.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition)
|
||||
}
|
||||
} else {
|
||||
strongSelf.currentPinchController?.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if case .pinnedMessages = self.presentationInterfaceState.subject {
|
||||
|
||||
@ -143,7 +143,6 @@ class ChatMessageShareButton: HighlightableButtonNode {
|
||||
class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
private let contextSourceNode: ContextExtractedContentContainingNode
|
||||
private let containerNode: ContextControllerSourceNode
|
||||
private let pinchContainerNode: PinchSourceContainerNode
|
||||
let imageNode: TransformImageNode
|
||||
private var placeholderNode: StickerShimmerEffectNode
|
||||
private var animationNode: GenericAnimatedStickerNode?
|
||||
@ -196,7 +195,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
required init() {
|
||||
self.contextSourceNode = ContextExtractedContentContainingNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
self.pinchContainerNode = PinchSourceContainerNode()
|
||||
self.imageNode = TransformImageNode()
|
||||
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
|
||||
|
||||
@ -264,8 +262,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.containerNode.addSubnode(self.contextSourceNode)
|
||||
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
|
||||
self.pinchContainerNode.contentNode.addSubnode(self.containerNode)
|
||||
self.addSubnode(self.pinchContainerNode)
|
||||
self.addSubnode(self.containerNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.imageNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.placeholderNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
|
||||
@ -281,23 +278,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
item.controllerInteraction.openMessageReactions(item.message.id)
|
||||
}
|
||||
|
||||
self.pinchContainerNode.activate = { [weak self] sourceNode in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.activateMessagePinch(sourceNode)
|
||||
}
|
||||
|
||||
self.pinchContainerNode.scaleUpdated = { [weak self] scale, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let factor: CGFloat = max(0.0, min(1.0, (scale - 1.0) * 8.0))
|
||||
|
||||
transition.updateAlpha(node: strongSelf.dateAndStatusNode, alpha: 1.0 - factor)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -996,8 +976,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
|
||||
strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.pinchContainerNode.update(size: layoutSize, transition: .immediate)
|
||||
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
|
||||
@ -1085,7 +1063,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
imageApply()
|
||||
|
||||
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
|
||||
strongSelf.pinchContainerNode.contentRect = strongSelf.imageNode.frame
|
||||
strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect
|
||||
|
||||
if let updatedShareButtonNode = updatedShareButtonNode {
|
||||
|
||||
@ -926,7 +926,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
}
|
||||
|
||||
videoNode.updateLayout(size: arguments.drawingSize, transition: .immediate)
|
||||
videoNode.frame = imageFrame
|
||||
videoNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size)
|
||||
|
||||
if strongSelf.visibility {
|
||||
if !videoNode.canAttachContent {
|
||||
|
||||
@ -21,7 +21,6 @@ private let inlineBotNameFont = nameFont
|
||||
class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
private let contextSourceNode: ContextExtractedContentContainingNode
|
||||
private let containerNode: ContextControllerSourceNode
|
||||
private let pinchContainerNode: PinchSourceContainerNode
|
||||
let imageNode: TransformImageNode
|
||||
private var placeholderNode: StickerShimmerEffectNode
|
||||
var textNode: TextNode?
|
||||
@ -54,7 +53,6 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
required init() {
|
||||
self.contextSourceNode = ContextExtractedContentContainingNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
self.pinchContainerNode = PinchSourceContainerNode()
|
||||
self.imageNode = TransformImageNode()
|
||||
self.placeholderNode = StickerShimmerEffectNode()
|
||||
self.placeholderNode.isUserInteractionEnabled = false
|
||||
@ -121,8 +119,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.containerNode.addSubnode(self.contextSourceNode)
|
||||
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
|
||||
self.pinchContainerNode.contentNode.addSubnode(self.containerNode)
|
||||
self.addSubnode(self.pinchContainerNode)
|
||||
self.addSubnode(self.containerNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.placeholderNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.imageNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
|
||||
@ -138,23 +135,6 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
item.controllerInteraction.openMessageReactions(item.message.id)
|
||||
}
|
||||
|
||||
self.pinchContainerNode.activate = { [weak self] sourceNode in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.activateMessagePinch(sourceNode)
|
||||
}
|
||||
|
||||
self.pinchContainerNode.scaleUpdated = { [weak self] scale, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let factor: CGFloat = max(0.0, min(1.0, (scale - 1.0) * 8.0))
|
||||
|
||||
transition.updateAlpha(node: strongSelf.dateAndStatusNode, alpha: 1.0 - factor)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
@ -675,12 +655,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
|
||||
strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.pinchContainerNode.update(size: layoutSize, transition: .immediate)
|
||||
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
|
||||
strongSelf.pinchContainerNode.contentRect = strongSelf.imageNode.frame
|
||||
strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect
|
||||
|
||||
dateAndStatusApply(false)
|
||||
|
||||
@ -4029,7 +4029,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
|
||||
let text: String
|
||||
switch error {
|
||||
case .generic:
|
||||
case .generic, .scheduledTooLate:
|
||||
text = strongSelf.presentationData.strings.Login_UnknownError
|
||||
case .anonymousNotAllowed:
|
||||
text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText
|
||||
|
||||
@ -16,6 +16,7 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry {
|
||||
public var enableVoipTcp: Bool
|
||||
public var demoVideoChats: Bool
|
||||
public var experimentalCompatibility: Bool
|
||||
public var enableNoiseSuppression: Bool
|
||||
|
||||
public static var defaultSettings: ExperimentalUISettings {
|
||||
return ExperimentalUISettings(
|
||||
@ -31,7 +32,8 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry {
|
||||
disableVideoAspectScaling: false,
|
||||
enableVoipTcp: false,
|
||||
demoVideoChats: false,
|
||||
experimentalCompatibility: false
|
||||
experimentalCompatibility: false,
|
||||
enableNoiseSuppression: false
|
||||
)
|
||||
}
|
||||
|
||||
@ -48,7 +50,8 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry {
|
||||
disableVideoAspectScaling: Bool,
|
||||
enableVoipTcp: Bool,
|
||||
demoVideoChats: Bool,
|
||||
experimentalCompatibility: Bool
|
||||
experimentalCompatibility: Bool,
|
||||
enableNoiseSuppression: Bool
|
||||
) {
|
||||
self.keepChatNavigationStack = keepChatNavigationStack
|
||||
self.skipReadHistory = skipReadHistory
|
||||
@ -63,6 +66,7 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry {
|
||||
self.enableVoipTcp = enableVoipTcp
|
||||
self.demoVideoChats = demoVideoChats
|
||||
self.experimentalCompatibility = experimentalCompatibility
|
||||
self.enableNoiseSuppression = enableNoiseSuppression
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
@ -79,6 +83,7 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry {
|
||||
self.enableVoipTcp = decoder.decodeInt32ForKey("enableVoipTcp", orElse: 0) != 0
|
||||
self.demoVideoChats = decoder.decodeInt32ForKey("demoVideoChats", orElse: 0) != 0
|
||||
self.experimentalCompatibility = decoder.decodeInt32ForKey("experimentalCompatibility", orElse: 0) != 0
|
||||
self.enableNoiseSuppression = decoder.decodeInt32ForKey("enableNoiseSuppression", orElse: 0) != 0
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
@ -97,6 +102,7 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry {
|
||||
encoder.encodeInt32(self.enableVoipTcp ? 1 : 0, forKey: "enableVoipTcp")
|
||||
encoder.encodeInt32(self.demoVideoChats ? 1 : 0, forKey: "demoVideoChats")
|
||||
encoder.encodeInt32(self.experimentalCompatibility ? 1 : 0, forKey: "experimentalCompatibility")
|
||||
encoder.encodeInt32(self.enableNoiseSuppression ? 1 : 0, forKey: "enableNoiseSuppression")
|
||||
}
|
||||
|
||||
public func isEqual(to: PreferencesEntry) -> Bool {
|
||||
|
||||
@ -180,7 +180,7 @@ public final class OngoingGroupCallContext {
|
||||
|
||||
private var broadcastPartsSource: BroadcastPartSource?
|
||||
|
||||
init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?, participantDescriptionsRequired: @escaping (Set<UInt32>) -> Void, audioStreamData: AudioStreamData?, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, enableVideo: Bool) {
|
||||
init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?, participantDescriptionsRequired: @escaping (Set<UInt32>) -> Void, audioStreamData: AudioStreamData?, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, enableVideo: Bool, enableNoiseSuppression: Bool) {
|
||||
self.queue = queue
|
||||
|
||||
var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)?
|
||||
@ -224,7 +224,7 @@ public final class OngoingGroupCallContext {
|
||||
},
|
||||
outgoingAudioBitrateKbit: outgoingAudioBitrateKbit ?? 32,
|
||||
enableVideo: enableVideo,
|
||||
enableNoiseSuppression: true
|
||||
enableNoiseSuppression: enableNoiseSuppression
|
||||
)
|
||||
|
||||
let queue = self.queue
|
||||
@ -529,10 +529,10 @@ public final class OngoingGroupCallContext {
|
||||
}
|
||||
}
|
||||
|
||||
public init(inputDeviceId: String = "", outputDeviceId: String = "", video: OngoingCallVideoCapturer?, participantDescriptionsRequired: @escaping (Set<UInt32>) -> Void, audioStreamData: AudioStreamData?, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, enableVideo: Bool) {
|
||||
public init(inputDeviceId: String = "", outputDeviceId: String = "", video: OngoingCallVideoCapturer?, participantDescriptionsRequired: @escaping (Set<UInt32>) -> Void, audioStreamData: AudioStreamData?, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, enableVideo: Bool, enableNoiseSuppression: Bool) {
|
||||
let queue = self.queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, video: video, participantDescriptionsRequired: participantDescriptionsRequired, audioStreamData: audioStreamData, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: enableVideo)
|
||||
return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, video: video, participantDescriptionsRequired: participantDescriptionsRequired, audioStreamData: audioStreamData, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, enableVideo: enableVideo, enableNoiseSuppression: enableNoiseSuppression)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"app": "7.6.2",
|
||||
"app": "7.7",
|
||||
"bazel": "4.0.0",
|
||||
"xcode": "12.4"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user