Merge commit '3481e44afacbac6bb69023ede58fb0d166a889de'

This commit is contained in:
Ali 2022-06-11 21:54:50 +01:00
commit b2477bc773
39 changed files with 1362 additions and 434 deletions

View File

@ -7564,6 +7564,15 @@ Sorry for the inconvenience.";
"OldChannels.TooManyCommunitiesText" = "You are a member of **%@** groups and channels. Please leave some before joining a new one or upgrade to **Telegram Premium** to double the limit to **%@** groups and channels."; "OldChannels.TooManyCommunitiesText" = "You are a member of **%@** groups and channels. Please leave some before joining a new one or upgrade to **Telegram Premium** to double the limit to **%@** groups and channels.";
"OldChannels.TooManyCommunitiesNoPremiumText" = "You are a member of **%@** groups and channels. Please leave some before joining a new one. We are working to let you increase this limit in the future."; "OldChannels.TooManyCommunitiesNoPremiumText" = "You are a member of **%@** groups and channels. Please leave some before joining a new one. We are working to let you increase this limit in the future.";
"OldChannels.TooManyCommunitiesFinalText" = "You are a member of **%@** groups and channels. Please leave some before joining a new one."; "OldChannels.TooManyCommunitiesFinalText" = "You are a member of **%@** groups and channels. Please leave some before joining a new one.";
"OldChannels.TooManyCommunitiesCreateText" = "You are a member of **%@** groups and channels. Please leave some before creating a new one or upgrade to **Telegram Premium** to double the limit to **%@** groups and channels.";
"OldChannels.TooManyCommunitiesCreateNoPremiumText" = "You are a member of **%@** groups and channels. Please leave some before creating a new one. We are working to let you increase this limit in the future.";
"OldChannels.TooManyCommunitiesCreateFinalText" = "You are a member of **%@** groups and channels. Please leave some before creating a new one.";
"OldChannels.TooManyCommunitiesUpgradeText" = "You are a member of **%@** groups and channels. For technical reasons, you need to leave some first before changing this setting in your groups or upgrade to **Telegram Premium** to double the limit to **%@** groups and channels.";
"OldChannels.TooManyCommunitiesUpgradeNoPremiumText" = "You are a member of **%@** groups and channels. For technical reasons, you need to leave some first before changing this setting in your groups. We are working to let you increase this limit in the future.";
"OldChannels.TooManyCommunitiesUpgradeFinalText" = "You are a member of **%@** groups and channels. For technical reasons, you need to leave some first before changing this setting in your groups.";
"OldChannels.LeaveCommunities_1" = "Leave %@ Community"; "OldChannels.LeaveCommunities_1" = "Leave %@ Community";
"OldChannels.LeaveCommunities_any" = "Leave %@ Communities"; "OldChannels.LeaveCommunities_any" = "Leave %@ Communities";
@ -7711,5 +7720,12 @@ Sorry for the inconvenience.";
"Premium.Purchase.ErrorUnknown" = "An error occurred. Please try again."; "Premium.Purchase.ErrorUnknown" = "An error occurred. Please try again.";
"Premium.Purchase.ErrorNetwork" = "Please check your internet connection and try again."; "Premium.Purchase.ErrorNetwork" = "Please check your internet connection and try again.";
"Premium.Purchase.ErrorNotAllowed" = "The device is not not allowed to make the payment."; "Premium.Purchase.ErrorNotAllowed" = "The device is not not allowed to make the payment.";
"Premium.Purchase.ErrorCantMakePayments" = "In-app purchases are not allowed on this device.";
"Premium.Restore.Success" = "Done";
"Premium.Restore.ErrorUnknown" = "An error occurred. Please try again.";
"Settings.Premium" = "Telegram Premium"; "Settings.Premium" = "Telegram Premium";
"Settings.AddAnotherAccount.PremiumHelp" = "You can add up to four accounts with different phone numbers.";

View File

@ -354,14 +354,26 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
case let .limitExceeded(count, _): case let .limitExceeded(count, _):
f(.default) f(.default)
var replaceImpl: ((ViewController) -> Void)? if case .filter = location {
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { var replaceImpl: ((ViewController) -> Void)?
let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
replaceImpl?(premiumScreen) let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats)
}) replaceImpl?(premiumScreen)
chatListController?.push(controller) })
replaceImpl = { [weak controller] c in chatListController?.push(controller)
controller?.replace(with: c) replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
} else {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {
let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats)
replaceImpl?(premiumScreen)
})
chatListController?.push(controller)
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
} }
} }
}) })

View File

@ -276,6 +276,23 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
transition = .immediate transition = .immediate
} }
if let reorderControlSizeAndApply = reorderControlSizeAndApply {
if strongSelf.reorderControlNode == nil {
let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate)
strongSelf.reorderControlNode = reorderControlNode
strongSelf.controlsContainer.addSubnode(reorderControlNode)
reorderControlNode.alpha = 0.0
transition.updateAlpha(node: reorderControlNode, alpha: 1.0)
}
let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0, y: 0.0), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height))
strongSelf.reorderControlNode?.frame = reorderControlFrame
} else if let reorderControlNode = strongSelf.reorderControlNode {
strongSelf.reorderControlNode = nil
transition.updateAlpha(node: reorderControlNode, alpha: 0.0, completion: { [weak reorderControlNode] _ in
reorderControlNode?.removeFromSupernode()
})
}
if let editableControlSizeAndApply = editableControlSizeAndApply { if let editableControlSizeAndApply = editableControlSizeAndApply {
let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height)) let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height))
if strongSelf.editableControlNode == nil { if strongSelf.editableControlNode == nil {
@ -306,24 +323,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
}) })
} }
strongSelf.editableControlNode?.isHidden = !item.canBeDeleted strongSelf.editableControlNode?.isHidden = !item.canBeDeleted
if let reorderControlSizeAndApply = reorderControlSizeAndApply {
if strongSelf.reorderControlNode == nil {
let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate)
strongSelf.reorderControlNode = reorderControlNode
strongSelf.addSubnode(reorderControlNode)
reorderControlNode.alpha = 0.0
transition.updateAlpha(node: reorderControlNode, alpha: 1.0)
}
let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0, y: 0.0), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height))
strongSelf.reorderControlNode?.frame = reorderControlFrame
} else if let reorderControlNode = strongSelf.reorderControlNode {
strongSelf.reorderControlNode = nil
transition.updateAlpha(node: reorderControlNode, alpha: 0.0, completion: { [weak reorderControlNode] _ in
reorderControlNode?.removeFromSupernode()
})
}
let _ = titleApply() let _ = titleApply()
let _ = labelApply() let _ = labelApply()

View File

@ -294,7 +294,7 @@ private final class ItemNode: ASDisplayNode {
if let deleteButtonNode = self.deleteButtonNode { if let deleteButtonNode = self.deleteButtonNode {
if let theme = self.theme { if let theme = self.theme {
let deleteButtonSize = deleteButtonNode.update(theme: theme) let deleteButtonSize = deleteButtonNode.update(theme: theme)
deleteButtonNode.frame = CGRect(origin: CGPoint(x: -deleteButtonSize.width, y: 5.0), size: deleteButtonSize) deleteButtonNode.frame = CGRect(origin: CGPoint(x: -deleteButtonSize.width + 3.0, y: 5.0), size: deleteButtonSize)
} }
} }
@ -788,7 +788,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
selectionFraction = 0.0 selectionFraction = 0.0
} }
itemNode.updateText(strings: presentationData.strings, title: filter.title(strings: presentationData.strings), shortTitle: filter.shortTitle(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, selectionFraction: selectionFraction, isEditing: isEditing, isReordering: isReordering, canReorderAllChats: canReorderAllChats, isDisabled: isDisabled, presentationData: presentationData, transition: itemNodeTransition) itemNode.updateText(strings: presentationData.strings, title: filter.title(strings: presentationData.strings), shortTitle: i == 0 ? filter.shortTitle(strings: presentationData.strings) : filter.title(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, selectionFraction: selectionFraction, isEditing: isEditing, isReordering: isReordering, canReorderAllChats: canReorderAllChats, isDisabled: isDisabled, presentationData: presentationData, transition: itemNodeTransition)
} }
var removeKeys: [ChatListFilterTabEntryId] = [] var removeKeys: [ChatListFilterTabEntryId] = []
for (id, _) in self.itemNodes { for (id, _) in self.itemNodes {

View File

@ -2241,7 +2241,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateFrameAdditive(node: self.titleNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: titleFrame.origin.y), size: titleFrame.size)) transition.updateFrameAdditive(node: self.titleNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: titleFrame.origin.y), size: titleFrame.size))
let authorFrame = self.authorNode.frame let authorFrame = self.authorNode.frame
transition.updateFrame(node: self.authorNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: authorFrame.origin.y), size: authorFrame.size)) transition.updateFrame(node: self.authorNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: authorFrame.origin.y), size: authorFrame.size))
transition.updateFrame(node: self.inputActivitiesNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: self.inputActivitiesNode.frame.minY), size: self.inputActivitiesNode.bounds.size)) transition.updateFrame(node: self.inputActivitiesNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: self.inputActivitiesNode.frame.minY), size: self.inputActivitiesNode.bounds.size))
@ -2253,7 +2253,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateFrameAdditive(node: dustNode, frame: textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)) transition.updateFrameAdditive(node: dustNode, frame: textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0))
} }
var mediaPreviewOffsetX = textFrame.origin.x + 1.0 var mediaPreviewOffsetX = textFrame.origin.x
let contentImageSpacing: CGFloat = 2.0 let contentImageSpacing: CGFloat = 2.0
for (_, media, mediaSize) in self.currentMediaPreviewSpecs { for (_, media, mediaSize) in self.currentMediaPreviewSpecs {
guard let mediaId = media.id else { guard let mediaId = media.id else {
@ -2279,7 +2279,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
} }
let mutedIconFrame = self.mutedIconNode.frame let mutedIconFrame = self.mutedIconNode.frame
transition.updateFrame(node: self.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin - 4.0, y: contentRect.origin.y - 2.0), size: mutedIconFrame.size)) transition.updateFrame(node: self.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin - 5.0, y: mutedIconFrame.minY), size: mutedIconFrame.size))
nextTitleIconOrigin += mutedIconFrame.size.width + 3.0 nextTitleIconOrigin += mutedIconFrame.size.width + 3.0
let badgeFrame = self.badgeNode.frame let badgeFrame = self.badgeNode.frame

View File

@ -932,7 +932,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
iconNode.isLayerBacked = true iconNode.isLayerBacked = true
iconNode.displaysAsynchronously = false iconNode.displaysAsynchronously = false
iconNode.displayWithoutProcessing = true iconNode.displayWithoutProcessing = true
strongSelf.containerNode.addSubnode(iconNode) strongSelf.offsetContainerNode.addSubnode(iconNode)
strongSelf.credibilityIconNode = iconNode strongSelf.credibilityIconNode = iconNode
} }
iconNode.image = currentCredibilityIconImage iconNode.image = currentCredibilityIconImage

View File

@ -30,6 +30,8 @@ public final class InAppPurchaseManager: NSObject {
case cancelled case cancelled
case network case network
case notAllowed case notAllowed
case cantMakePayments
case assignFailed
} }
public enum RestoreState { public enum RestoreState {
@ -51,6 +53,7 @@ public final class InAppPurchaseManager: NSObject {
case restored(transactionId: String?) case restored(transactionId: String?)
case purchasing case purchasing
case failed(error: SKError?) case failed(error: SKError?)
case assignFailed
case deferred case deferred
} }
@ -63,7 +66,7 @@ public final class InAppPurchaseManager: NSObject {
private let stateQueue = Queue() private let stateQueue = Queue()
private var paymentContexts: [String: PaymentTransactionContext] = [:] private var paymentContexts: [String: PaymentTransactionContext] = [:]
private var onRestoreCompletion: ((RestoreState) -> Void)? private var onRestoreCompletion: ((RestoreState) -> Void)?
private let disposableSet = DisposableDict<String>() private let disposableSet = DisposableDict<String>()
@ -82,6 +85,10 @@ public final class InAppPurchaseManager: NSObject {
SKPaymentQueue.default().remove(self) SKPaymentQueue.default().remove(self)
} }
var canMakePayments: Bool {
return SKPaymentQueue.canMakePayments()
}
private func requestProducts() { private func requestProducts() {
guard !self.premiumProductId.isEmpty else { guard !self.premiumProductId.isEmpty else {
return return
@ -119,10 +126,16 @@ public final class InAppPurchaseManager: NSObject {
} }
} }
public func buyProduct(_ product: Product, account: Account) -> Signal<PurchaseState, PurchaseError> { public func buyProduct(_ product: Product) -> Signal<PurchaseState, PurchaseError> {
Logger.shared.log("InAppPurchaseManager", "Buying product: \(product.skProduct.productIdentifier), price \(product.price)") if !self.canMakePayments {
return .fail(.cantMakePayments)
}
let accountPeerId = "\(self.engine.account.peerId.toInt64())"
let payment = SKPayment(product: product.skProduct) Logger.shared.log("InAppPurchaseManager", "Buying: account \(accountPeerId), product \(product.skProduct.productIdentifier), price \(product.price)")
let payment = SKMutablePayment(product: product.skProduct)
payment.applicationUsername = accountPeerId
SKPaymentQueue.default().add(payment) SKPaymentQueue.default().add(payment)
let productIdentifier = payment.productIdentifier let productIdentifier = payment.productIdentifier
@ -156,6 +169,8 @@ public final class InAppPurchaseManager: NSObject {
} else { } else {
subscriber.putError(.generic) subscriber.putError(.generic)
} }
case .assignFailed:
subscriber.putError(.assignFailed)
case .deferred, .purchasing: case .deferred, .purchasing:
break break
} }
@ -204,40 +219,35 @@ private func getReceiptData() -> Data? {
extension InAppPurchaseManager: SKPaymentTransactionObserver { extension InAppPurchaseManager: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions { self.stateQueue.async {
let productIdentifier = transaction.payment.productIdentifier let accountPeerId = "\(self.engine.account.peerId.toInt64())"
self.stateQueue.async { var transactionsToAssign: [SKPaymentTransaction] = []
for transaction in transactions {
if let applicationUsername = transaction.payment.applicationUsername, applicationUsername != accountPeerId {
continue
}
let productIdentifier = transaction.payment.productIdentifier
let transactionState: TransactionState? let transactionState: TransactionState?
switch transaction.transactionState { switch transaction.transactionState {
case .purchased: case .purchased:
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "none") purchased") Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "none") purchased")
let transactionIdentifier = transaction.transactionIdentifier
transactionState = .purchased(transactionId: transactionIdentifier) transactionState = .purchased(transactionId: transaction.transactionIdentifier)
if let transactionIdentifier = transactionIdentifier { transactionsToAssign.append(transaction)
self.disposableSet.set(
self.engine.payments.sendAppStoreReceipt(receipt: getReceiptData() ?? Data(), restore: false).start(error: { _ in
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? "") failed to assign AppStore transaction")
queue.finishTransaction(transaction)
}, completed: {
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? "") successfully assigned AppStore transaction")
queue.finishTransaction(transaction)
}),
forKey: transactionIdentifier
)
}
case .restored: case .restored:
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "") restroring") Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "") restroring")
let transactionIdentifier = transaction.transactionIdentifier let transactionIdentifier = transaction.transactionIdentifier
transactionState = .restored(transactionId: transactionIdentifier) transactionState = .restored(transactionId: transactionIdentifier)
case .failed: case .failed:
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? "") failed \((transaction.error as? SKError)?.localizedDescription ?? "")") Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") failed \((transaction.error as? SKError)?.localizedDescription ?? "")")
transactionState = .failed(error: transaction.error as? SKError) transactionState = .failed(error: transaction.error as? SKError)
queue.finishTransaction(transaction) queue.finishTransaction(transaction)
case .purchasing: case .purchasing:
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? "") purchasing") Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") purchasing")
transactionState = .purchasing transactionState = .purchasing
case .deferred: case .deferred:
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? "") deferred") Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") deferred")
transactionState = .deferred transactionState = .deferred
default: default:
transactionState = nil transactionState = nil
@ -248,31 +258,60 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
} }
} }
} }
}
}
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
if let onRestoreCompletion = self.onRestoreCompletion {
Logger.shared.log("InAppPurchaseManager", "Transactions restoration finished")
onRestoreCompletion(.succeed)
self.onRestoreCompletion = nil
if let receiptData = getReceiptData() { if !transactionsToAssign.isEmpty {
let transactionIds = transactionsToAssign.compactMap({ $0.transactionIdentifier }).joined(separator: ", ")
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), sending receipt for transactions [\(transactionIds)]")
self.disposableSet.set( self.disposableSet.set(
self.engine.payments.sendAppStoreReceipt(receipt: receiptData, restore: true).start(completed: { self.engine.payments.sendAppStoreReceipt(receipt: getReceiptData() ?? Data(), restore: false).start(error: { [weak self] _ in
Logger.shared.log("InAppPurchaseManager", "Sent restored receipt") Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transactions [\(transactionIds)] failed to assign")
for transaction in transactions {
self?.stateQueue.async {
if let strongSelf = self, let context = strongSelf.paymentContexts[transaction.payment.productIdentifier] {
context.subscriber(.assignFailed)
}
}
queue.finishTransaction(transaction)
}
}, completed: {
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transactions [\(transactionIds)] successfully assigned")
for transaction in transactions {
queue.finishTransaction(transaction)
}
}), }),
forKey: "restore" forKey: transactionIds
) )
} }
} }
} }
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
Queue.mainQueue().async {
if let onRestoreCompletion = self.onRestoreCompletion {
Logger.shared.log("InAppPurchaseManager", "Transactions restoration finished")
onRestoreCompletion(.succeed)
self.onRestoreCompletion = nil
if let receiptData = getReceiptData() {
self.disposableSet.set(
self.engine.payments.sendAppStoreReceipt(receipt: receiptData, restore: true).start(completed: {
Logger.shared.log("InAppPurchaseManager", "Sent restored receipt")
}),
forKey: "restore"
)
}
}
}
}
public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) { public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
if let onRestoreCompletion = self.onRestoreCompletion { Queue.mainQueue().async {
Logger.shared.log("InAppPurchaseManager", "Transactions restoration failed with error \((error as? SKError)?.localizedDescription ?? "")") if let onRestoreCompletion = self.onRestoreCompletion {
onRestoreCompletion(.failed) Logger.shared.log("InAppPurchaseManager", "Transactions restoration failed with error \((error as? SKError)?.localizedDescription ?? "")")
self.onRestoreCompletion = nil onRestoreCompletion(.failed)
self.onRestoreCompletion = nil
}
} }
} }
} }

View File

@ -0,0 +1,57 @@
//import Foundation
//import UIKit
//import SwiftSignalKit
//import Postbox
//import TelegramCore
//import TelegramUIPreferences
//
//final class StoredTransactionState: Codable {
// let timestamp: Double
// let playbackRate: AudioPlaybackRate
//
// init(timestamp: Double, playbackRate: AudioPlaybackRate) {
// self.timestamp = timestamp
// self.playbackRate = playbackRate
// }
//
// public init(from decoder: Decoder) throws {
// let container = try decoder.container(keyedBy: StringCodingKey.self)
//
// self.timestamp = try container.decode(Double.self, forKey: "timestamp")
// self.playbackRate = AudioPlaybackRate(rawValue: try container.decode(Int32.self, forKey: "playbackRate")) ?? .x1
// }
//
// public func encode(to encoder: Encoder) throws {
// var container = encoder.container(keyedBy: StringCodingKey.self)
//
// try container.encode(self.timestamp, forKey: "timestamp")
// try container.encode(self.playbackRate.rawValue, forKey: "playbackRate")
// }
//}
//
//public func storedState(engine: TelegramEngine, : MessageId) -> Signal<MediaPlaybackStoredState?, NoError> {
// let key = ValueBoxKey(length: 20)
// key.setInt32(0, value: messageId.namespace)
// key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
// key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value())
// key.setInt32(16, value: messageId.id)
//
// return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.mediaPlaybackStoredState, id: key))
// |> map { entry -> MediaPlaybackStoredState? in
// return entry?.get(MediaPlaybackStoredState.self)
// }
//}
//
//public func updateMediaPlaybackStoredStateInteractively(engine: TelegramEngine, messageId: MessageId, state: MediaPlaybackStoredState?) -> Signal<Never, NoError> {
// let key = ValueBoxKey(length: 20)
// key.setInt32(0, value: messageId.namespace)
// key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
// key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value())
// key.setInt32(16, value: messageId.id)
//
// if let state = state {
// return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.mediaPlaybackStoredState, id: key, item: state)
// } else {
// return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.mediaPlaybackStoredState, id: key)
// }
//}

View File

@ -191,12 +191,33 @@ private func oldChannelsEntries(presentationData: PresentationData, state: OldCh
let count = max(limit, Int32(peers?.count ?? 0)) let count = max(limit, Int32(peers?.count ?? 0))
var text: String? var text: String?
if count >= premiumLimit { if count >= premiumLimit {
text = presentationData.strings.OldChannels_TooManyCommunitiesFinalText("\(premiumLimit)").string switch intent {
case .create:
text = presentationData.strings.OldChannels_TooManyCommunitiesCreateFinalText("\(premiumLimit)").string
case .upgrade:
text = presentationData.strings.OldChannels_TooManyCommunitiesUpgradeFinalText("\(premiumLimit)").string
case .join:
text = presentationData.strings.OldChannels_TooManyCommunitiesFinalText("\(premiumLimit)").string
}
} else if count >= limit { } else if count >= limit {
if isPremiumDisabled { if isPremiumDisabled {
text = presentationData.strings.OldChannels_TooManyCommunitiesNoPremiumText("\(count)").string switch intent {
case .create:
text = presentationData.strings.OldChannels_TooManyCommunitiesCreateNoPremiumText("\(premiumLimit)").string
case .upgrade:
text = presentationData.strings.OldChannels_TooManyCommunitiesUpgradeNoPremiumText("\(premiumLimit)").string
case .join:
text = presentationData.strings.OldChannels_TooManyCommunitiesNoPremiumText("\(count)").string
}
} else { } else {
text = presentationData.strings.OldChannels_TooManyCommunitiesText("\(count)", "\(premiumLimit)").string switch intent {
case .create:
text = presentationData.strings.OldChannels_TooManyCommunitiesCreateText("\(count)", "\(premiumLimit)").string
case .upgrade:
text = presentationData.strings.OldChannels_TooManyCommunitiesUpgradeText("\(count)", "\(premiumLimit)").string
case .join:
text = presentationData.strings.OldChannels_TooManyCommunitiesText("\(count)", "\(premiumLimit)").string
}
} }
} }

View File

@ -1,4 +1,44 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load(
"@build_bazel_rules_apple//apple:resources.bzl",
"apple_resource_bundle",
"apple_resource_group",
)
load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
filegroup(
name = "PremiumUIMetalResources",
srcs = glob([
"MetalResources/**/*.*",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "PremiumUIBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.PremiumUI</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>PremiumUI</string>
"""
)
apple_resource_bundle(
name = "PremiumUIBundle",
infoplists = [
":PremiumUIBundleInfoPlist",
],
resources = [
":PremiumUIMetalResources",
],
)
filegroup( filegroup(
name = "PremiumUIResources", name = "PremiumUIResources",
@ -17,6 +57,9 @@ swift_library(
copts = [ copts = [
"-warnings-as-errors", "-warnings-as-errors",
], ],
data = [
":PremiumUIBundle",
],
deps = [ deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit",

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,79 @@
#include <metal_stdlib>
using namespace metal;
typedef struct {
packed_float2 position;
} Vertex;
struct RasterizerData
{
float4 position [[position]];
};
vertex RasterizerData matrixVertex
(
constant Vertex *vertexArray[[buffer(0)]],
uint vertexID [[ vertex_id ]]
) {
RasterizerData out;
out.position = vector_float4(vertexArray[vertexID].position[0], vertexArray[vertexID].position[1], 0.0, 1.0);
return out;
}
float text(float2 uvIn,
texture2d<half> symbolTexture,
texture2d<float> noiseTexture,
float time)
{
constexpr sampler textureSampler(min_filter::linear, mag_filter::linear, mip_filter::linear, address::repeat);
float count = 32.0;
float2 noiseResolution = float2(256.0, 256.0);
float2 uv = fmod(uvIn, 1.0 / count) * count;
float2 block = uvIn * count - uv;
uv = uv * 0.8;
uv += floor(noiseTexture.sample(textureSampler, block / noiseResolution + time * .00025).xy * 256.);
uv *= -1.0;
uv *= 0.25;
return symbolTexture.sample(textureSampler, uv).g;
}
float4 rain(float2 uvIn,
uint2 resolution,
float time)
{
float count = 32.0;
uvIn.x -= fmod(uvIn.x, 1.0 / count);
uvIn.y -= fmod(uvIn.y, 1.0 / count);
float2 fragCoord = uvIn * float2(resolution);
float offset = sin(fragCoord.x * 15.0);
float speed = cos(fragCoord.x * 3.0) * 0.3 + 0.7;
float y = fract(fragCoord.y / resolution.y + time * speed + offset);
return float4(1.0, 1.0, 1.0, 1.0 / (y * 30.0) - 0.02);
}
fragment half4 matrixFragment(RasterizerData in[[stage_in]],
texture2d<half> symbolTexture [[ texture(0) ]],
texture2d<float> noiseTexture [[ texture(1) ]],
constant uint2 &resolution[[buffer(0)]],
constant float &time[[buffer(1)]])
{
float2 uv = (in.position.xy / float2(resolution.xy) - float2(0.5, 0.5));
uv.y -= 0.1;
float2 lookup = float2(0.08 / (uv.x), (0.9 - abs(uv.x)) * uv.y * -1.0) * 2.0;
float4 out = text(lookup, symbolTexture, noiseTexture, time) * rain(lookup, resolution, time);
return half4(out);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

View File

@ -0,0 +1,59 @@
import Foundation
import UIKit
import SceneKit
import Display
import AppBundle
final class BadgeStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
private var leftParticles: SCNNode?
private var rightParticles: SCNNode?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "badge", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.alpha = 0.0
self.addSubview(self.sceneView)
self.leftParticles = self.sceneView.scene?.rootNode.childNode(withName: "leftParticles", recursively: false)
self.rightParticles = self.sceneView.scene?.rootNode.childNode(withName: "rightParticles", recursively: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setVisible(_ visible: Bool) {
if visible, let leftParticles = self.leftParticles, let rightParticles = self.rightParticles, leftParticles.parent == nil {
self.sceneView.scene?.rootNode.addChildNode(leftParticles)
self.sceneView.scene?.rootNode.addChildNode(rightParticles)
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.5 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible && strongSelf.leftParticles?.parent != nil {
strongSelf.leftParticles?.removeFromParentNode()
strongSelf.rightParticles?.removeFromParentNode()
}
})
}
func resetAnimation() {
}
override func layoutSubviews() {
super.layoutSubviews()
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
}
}

View File

@ -0,0 +1,198 @@
import Foundation
import Metal
import MetalKit
import Display
@available(iOS 10.0, *)
public final class MatrixView: MTKView, MTKViewDelegate, PhoneDemoDecorationView {
public func draw(in view: MTKView) {
}
private let commandQueue: MTLCommandQueue
private let drawPassthroughPipelineState: MTLRenderPipelineState
private var displayLink: CADisplayLink?
// private var metalLayer: CAMetalLayer {
// return self.layer as! CAMetalLayer
// }
private let symbolTexture: MTLTexture
private let randomTexture: MTLTexture
private var viewportDimensions = CGSize(width: 1, height: 1)
private var startTimestamp = CACurrentMediaTime()
public init?(test: Bool) {
let mainBundle = Bundle(for: MatrixView.self)
guard let path = mainBundle.path(forResource: "PremiumUIBundle", ofType: "bundle") else {
return nil
}
guard let bundle = Bundle(path: path) else {
return nil
}
guard let device = MTLCreateSystemDefaultDevice() else {
return nil
}
guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else {
return nil
}
guard let commandQueue = device.makeCommandQueue() else {
return nil
}
self.commandQueue = commandQueue
guard let loadedVertexProgram = defaultLibrary.makeFunction(name: "matrixVertex") else {
return nil
}
guard let loadedFragmentProgram = defaultLibrary.makeFunction(name: "matrixFragment") else {
return nil
}
let textureLoader = MTKTextureLoader(device: device)
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.vertexFunction = loadedVertexProgram
pipelineStateDescriptor.fragmentFunction = loadedFragmentProgram
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineStateDescriptor.colorAttachments[0].isBlendingEnabled = true
pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = .add
pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = .add
pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
pipelineStateDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
pipelineStateDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
self.drawPassthroughPipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
guard let url = bundle.url(forResource: "chars", withExtension: "png"), let texture = try? textureLoader.newTexture(URL: url, options: nil) else {
return nil
}
self.symbolTexture = texture
guard let url = bundle.url(forResource: "random", withExtension: "jpg"), let texture = try? textureLoader.newTexture(URL: url, options: nil) else {
return nil
}
self.randomTexture = texture
super.init(frame: CGRect(), device: device)
self.delegate = self
self.isOpaque = false
self.backgroundColor = .clear
self.framebufferOnly = true
class DisplayLinkProxy: NSObject {
weak var target: MatrixView?
init(target: MatrixView) {
self.target = target
}
@objc func displayLinkEvent() {
self.target?.displayLinkEvent()
}
}
self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent))
if #available(iOS 15.0, *) {
self.displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: 60.0, preferred: 60.0)
}
self.displayLink?.add(to: .main, forMode: .common)
self.displayLink?.isPaused = false
self.isPaused = true
}
public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
self.viewportDimensions = size
}
required public init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.displayLink?.invalidate()
}
func setVisible(_ visible: Bool) {
if visible {
self.displayLink?.isPaused = false
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.4 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible {
strongSelf.displayLink?.isPaused = false
}
})
}
func resetAnimation() {
}
@objc private func displayLinkEvent() {
self.draw()
}
override public func draw(_ rect: CGRect) {
self.redraw(drawable: self.currentDrawable!)
}
private func redraw(drawable: MTLDrawable) {
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
return
}
let renderPassDescriptor = self.currentRenderPassDescriptor!
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0.0)
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
return
}
let viewportDimensions = self.viewportDimensions
renderEncoder.setViewport(MTLViewport(originX: 0.0, originY: 0.0, width: viewportDimensions.width, height: viewportDimensions.height, znear: -1.0, zfar: 1.0))
renderEncoder.setRenderPipelineState(self.drawPassthroughPipelineState)
var vertices: [Float] = [
1, -1,
-1, -1,
-1, 1,
1, -1,
-1, 1,
1, 1
]
renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0)
renderEncoder.setFragmentTexture(self.symbolTexture, index: 0)
renderEncoder.setFragmentTexture(self.randomTexture, index: 1)
var resolution = simd_uint2(UInt32(viewportDimensions.width), UInt32(viewportDimensions.height))
renderEncoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
var time = Float(CACurrentMediaTime() - self.startTimestamp) * 0.75
renderEncoder.setFragmentBytes(&time, length: 4, index: 1)
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1)
renderEncoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
}
}

View File

@ -0,0 +1,107 @@
import Foundation
import UIKit
import SceneKit
import Display
import AppBundle
final class FasterStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
private var particles: SCNNode?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "lightspeed", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.alpha = 0.0
self.addSubview(self.sceneView)
self.particles = self.sceneView.scene?.rootNode.childNode(withName: "particles", recursively: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.particles = nil
}
func setVisible(_ visible: Bool) {
if visible, let particles = self.particles, particles.parent == nil {
self.sceneView.scene?.rootNode.addChildNode(particles)
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.4 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible && strongSelf.particles?.parent != nil {
strongSelf.particles?.removeFromParentNode()
}
})
}
private var playing = false
func startAnimation() {
guard !self.playing, let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "particles", recursively: false), let particles = node.particleSystems?.first else {
return
}
self.playing = true
let speedAnimation = CABasicAnimation(keyPath: "speedFactor")
speedAnimation.fromValue = 1.0
speedAnimation.toValue = 1.8
speedAnimation.duration = 0.8
speedAnimation.fillMode = .forwards
particles.addAnimation(speedAnimation, forKey: "speedFactor")
particles.speedFactor = 3.0
let stretchAnimation = CABasicAnimation(keyPath: "stretchFactor")
stretchAnimation.fromValue = 0.05
stretchAnimation.toValue = 0.3
stretchAnimation.duration = 0.8
stretchAnimation.fillMode = .forwards
particles.addAnimation(stretchAnimation, forKey: "stretchFactor")
particles.stretchFactor = 0.3
}
func resetAnimation() {
guard self.playing, let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "particles", recursively: false), let particles = node.particleSystems?.first else {
return
}
self.playing = false
let speedAnimation = CABasicAnimation(keyPath: "speedFactor")
speedAnimation.fromValue = 3.0
speedAnimation.toValue = 1.0
speedAnimation.duration = 0.35
speedAnimation.fillMode = .forwards
particles.addAnimation(speedAnimation, forKey: "speedFactor")
particles.speedFactor = 1.0
let stretchAnimation = CABasicAnimation(keyPath: "stretchFactor")
stretchAnimation.fromValue = 0.3
stretchAnimation.toValue = 0.05
stretchAnimation.duration = 0.35
stretchAnimation.fillMode = .forwards
particles.addAnimation(stretchAnimation, forKey: "stretchFactor")
particles.stretchFactor = 0.05
}
override func layoutSubviews() {
super.layoutSubviews()
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
}
}

View File

@ -111,6 +111,7 @@ private final class PhoneView: UIView {
self.shimmerMaskView = UIView() self.shimmerMaskView = UIView()
self.shimmerBorderView = UIImageView(image: phoneBorderMaskImage) self.shimmerBorderView = UIImageView(image: phoneBorderMaskImage)
self.shimmerStarView = UIImageView(image: starMaskImage) self.shimmerStarView = UIImageView(image: starMaskImage)
self.shimmerStarView.alpha = 0.7
self.backShimmerView = UIView() self.backShimmerView = UIView()
self.backShimmerView.alpha = 0.0 self.backShimmerView.alpha = 0.0
@ -140,10 +141,10 @@ private final class PhoneView: UIView {
self.frontShimmerView.mask = self.shimmerMaskView self.frontShimmerView.mask = self.shimmerMaskView
self.frontShimmerView.addSubview(self.shimmerEffectView) self.frontShimmerView.addSubview(self.shimmerEffectView)
self.backShimmerEffectView.update(backgroundColor: .clear, foregroundColor: UIColor.white.withAlphaComponent(0.35), gradientSize: 70.0, globalTimeOffset: true, duration: 3.0, horizontal: true) self.backShimmerEffectView.update(backgroundColor: .clear, foregroundColor: UIColor.white.withAlphaComponent(0.35), gradientSize: 60.0, globalTimeOffset: true, duration: 4.0, horizontal: true)
self.backShimmerEffectView.layer.compositingFilter = "overlayBlendMode" self.backShimmerEffectView.layer.compositingFilter = "overlayBlendMode"
self.shimmerEffectView.update(backgroundColor: .clear, foregroundColor: UIColor.white.withAlphaComponent(0.65), gradientSize: 70.0, globalTimeOffset: true, duration: 3.0, horizontal: true) self.shimmerEffectView.update(backgroundColor: .clear, foregroundColor: UIColor.white.withAlphaComponent(0.5), gradientSize: 16.0, globalTimeOffset: true, duration: 4.0, horizontal: true)
self.shimmerEffectView.layer.compositingFilter = "overlayBlendMode" self.shimmerEffectView.layer.compositingFilter = "overlayBlendMode"
} }
@ -180,8 +181,16 @@ private final class PhoneView: UIView {
let status = videoNode.status let status = videoNode.status
|> mapToSignal { status -> Signal<MediaPlayerStatus?, NoError> in |> mapToSignal { status -> Signal<MediaPlayerStatus?, NoError> in
if let status = status, case .buffering = status.status { var isLoading = false
return .single(status) |> delay(1.0, queue: Queue.mainQueue()) if let status = status {
if case .buffering = status.status {
isLoading = true
} else if status.duration.isZero {
isLoading = true
}
}
if isLoading {
return .single(status) |> delay(0.6, queue: Queue.mainQueue())
} else { } else {
return .single(status) return .single(status)
} }
@ -205,11 +214,13 @@ private final class PhoneView: UIView {
private func updatePlaybackStatus() { private func updatePlaybackStatus() {
var isDisplayingProgress = false var isDisplayingProgress = false
if let playbackStatus = self.playbackStatusValue { if let playbackStatus = self.playbackStatusValue {
if case let .buffering(initial, _, progress, _) = playbackStatus.status, initial || !progress.isZero { if case .buffering = playbackStatus.status {
isDisplayingProgress = true isDisplayingProgress = true
} else if playbackStatus.status == .playing { } else if playbackStatus.status == .playing {
isDisplayingProgress = false isDisplayingProgress = playbackStatus.duration.isZero
} }
} else {
isDisplayingProgress = true
} }
let targetAlpha = isDisplayingProgress ? 1.0 : 0.0 let targetAlpha = isDisplayingProgress ? 1.0 : 0.0
@ -280,125 +291,9 @@ private final class PhoneView: UIView {
} }
} }
private final class FasterStarsView: UIView { protocol PhoneDemoDecorationView: UIView {
private let sceneView: SCNView func setVisible(_ visible: Bool)
func resetAnimation()
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "lightspeed", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.alpha = 0.0
self.addSubview(self.sceneView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setVisible(_ visible: Bool) {
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 1.0 : 0.0)
}
private var playing = false
func startAnimation() {
guard !self.playing, let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "particles", recursively: false), let particles = node.particleSystems?.first else {
return
}
self.playing = true
let speedAnimation = CABasicAnimation(keyPath: "speedFactor")
speedAnimation.fromValue = 1.0
speedAnimation.toValue = 1.8
speedAnimation.duration = 0.8
speedAnimation.fillMode = .forwards
particles.addAnimation(speedAnimation, forKey: "speedFactor")
particles.speedFactor = 3.0
let stretchAnimation = CABasicAnimation(keyPath: "stretchFactor")
stretchAnimation.fromValue = 0.05
stretchAnimation.toValue = 0.3
stretchAnimation.duration = 0.8
stretchAnimation.fillMode = .forwards
particles.addAnimation(stretchAnimation, forKey: "stretchFactor")
particles.stretchFactor = 0.3
}
func stopAnimation() {
guard self.playing, let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "particles", recursively: false), let particles = node.particleSystems?.first else {
return
}
self.playing = false
let speedAnimation = CABasicAnimation(keyPath: "speedFactor")
speedAnimation.fromValue = 3.0
speedAnimation.toValue = 1.0
speedAnimation.duration = 0.35
speedAnimation.fillMode = .forwards
particles.addAnimation(speedAnimation, forKey: "speedFactor")
particles.speedFactor = 1.0
let stretchAnimation = CABasicAnimation(keyPath: "stretchFactor")
stretchAnimation.fromValue = 0.3
stretchAnimation.toValue = 0.05
stretchAnimation.duration = 0.35
stretchAnimation.fillMode = .forwards
particles.addAnimation(stretchAnimation, forKey: "stretchFactor")
particles.stretchFactor = 0.05
}
override func layoutSubviews() {
super.layoutSubviews()
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
}
}
private final class BadgeStarsView: UIView {
private let sceneView: SCNView
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "badge", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.alpha = 0.0
self.addSubview(self.sceneView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setVisible(_ visible: Bool) {
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.75 : 0.0)
}
override func layoutSubviews() {
super.layoutSubviews()
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
}
} }
final class PhoneDemoComponent: Component { final class PhoneDemoComponent: Component {
@ -411,6 +306,8 @@ final class PhoneDemoComponent: Component {
enum BackgroundDecoration { enum BackgroundDecoration {
case none case none
case dataRain
case swirlStars
case fasterStars case fasterStars
case badgeStars case badgeStars
} }
@ -462,14 +359,12 @@ final class PhoneDemoComponent: Component {
private var isCentral = false private var isCentral = false
private var component: PhoneDemoComponent? private var component: PhoneDemoComponent?
private let starsContainerView: UIView private let decorationContainerView: UIView
private var decorationView: PhoneDemoDecorationView?
private let containerView: UIView private let containerView: UIView
private let phoneView: PhoneView private let phoneView: PhoneView
private var fasterStarsView: FasterStarsView? private var playbackStatusDisposable: Disposable?
private var badgeStarsView: BadgeStarsView?
private var starsDisposable: Disposable?
public var ready: Signal<Bool, NoError> { public var ready: Signal<Bool, NoError> {
if let videoNode = self.phoneView.videoNode { if let videoNode = self.phoneView.videoNode {
@ -483,8 +378,8 @@ final class PhoneDemoComponent: Component {
} }
public override init(frame: CGRect) { public override init(frame: CGRect) {
self.starsContainerView = UIView(frame: frame) self.decorationContainerView = UIView(frame: frame)
self.starsContainerView.clipsToBounds = true self.decorationContainerView.clipsToBounds = true
self.containerView = UIView(frame: frame) self.containerView = UIView(frame: frame)
self.containerView.clipsToBounds = true self.containerView.clipsToBounds = true
@ -493,7 +388,7 @@ final class PhoneDemoComponent: Component {
super.init(frame: frame) super.init(frame: frame)
self.addSubview(self.starsContainerView) self.addSubview(self.decorationContainerView)
self.addSubview(self.containerView) self.addSubview(self.containerView)
self.containerView.addSubview(self.phoneView) self.containerView.addSubview(self.phoneView)
} }
@ -503,41 +398,59 @@ final class PhoneDemoComponent: Component {
} }
deinit { deinit {
self.starsDisposable?.dispose() self.playbackStatusDisposable?.dispose()
} }
public func update(component: PhoneDemoComponent, availableSize: CGSize, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize { public func update(component: PhoneDemoComponent, availableSize: CGSize, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
self.component = component self.component = component
self.containerView.frame = CGRect(origin: .zero, size: availableSize) self.containerView.frame = CGRect(origin: .zero, size: availableSize)
self.starsContainerView.frame = CGRect(origin: CGPoint(x: -availableSize.width * 0.5, y: 0.0), size: CGSize(width: availableSize.width * 2.0, height: availableSize.height)) self.decorationContainerView.frame = CGRect(origin: CGPoint(x: -availableSize.width * 0.5, y: 0.0), size: CGSize(width: availableSize.width * 2.0, height: availableSize.height))
self.phoneView.bounds = CGRect(origin: .zero, size: phoneSize) self.phoneView.bounds = CGRect(origin: .zero, size: phoneSize)
switch component.decoration { switch component.decoration {
case .none: case .none:
break break
case .dataRain:
if #available(iOS 10.0, *) {
if let _ = self.decorationView as? MatrixView {
} else if let rainView = MatrixView(test: true) {
rainView.frame = self.decorationContainerView.bounds.insetBy(dx: availableSize.width * 0.5, dy: 0.0)
self.decorationView = rainView
self.decorationContainerView.addSubview(rainView)
}
}
case .swirlStars:
if let _ = self.decorationView as? SwirlStarsView {
} else {
let starsView = SwirlStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
}
case .fasterStars: case .fasterStars:
if self.fasterStarsView == nil { if let _ = self.decorationView as? FasterStarsView {
let starsView = FasterStarsView(frame: self.starsContainerView.bounds) } else {
self.fasterStarsView = starsView let starsView = FasterStarsView(frame: self.decorationContainerView.bounds)
self.starsContainerView.addSubview(starsView) self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
self.starsDisposable = (self.phoneView.playbackStatus self.playbackStatusDisposable = (self.phoneView.playbackStatus
|> deliverOnMainQueue).start(next: { [weak self] status in |> deliverOnMainQueue).start(next: { [weak starsView] status in
if let strongSelf = self, let status = status { if let starsView = starsView, let status = status {
if status.timestamp > 8.0 { if status.timestamp > 8.0 {
strongSelf.fasterStarsView?.stopAnimation() starsView.resetAnimation()
} else if status.timestamp > 0.85 { } else if status.timestamp > 0.85 {
strongSelf.fasterStarsView?.startAnimation() starsView.startAnimation()
} }
} }
}) })
} }
case .badgeStars: case .badgeStars:
if self.badgeStarsView == nil { if let _ = self.decorationView as? BadgeStarsView {
let starsView = BadgeStarsView(frame: self.starsContainerView.bounds) } else {
self.badgeStarsView = starsView let starsView = BadgeStarsView(frame: self.decorationContainerView.bounds)
self.starsContainerView.addSubview(starsView) self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
} }
} }
@ -561,8 +474,9 @@ final class PhoneDemoComponent: Component {
let isCentral = environment[DemoPageEnvironment.self].isCentral let isCentral = environment[DemoPageEnvironment.self].isCentral
self.isCentral = isCentral self.isCentral = isCentral
self.fasterStarsView?.setVisible(isVisible && abs(mappedPosition) < 0.4) if let decorationView = self.decorationView {
self.badgeStarsView?.setVisible(isVisible && abs(mappedPosition) < 0.4) decorationView.setVisible(isVisible && abs(mappedPosition) < 0.4)
}
self.phoneView.center = CGPoint(x: availableSize.width / 2.0 + phoneX, y: phoneY) self.phoneView.center = CGPoint(x: availableSize.width / 2.0 + phoneX, y: phoneY)
self.phoneView.screenRotation = mappedPosition * -0.7 self.phoneView.screenRotation = mappedPosition * -0.7
@ -575,7 +489,7 @@ final class PhoneDemoComponent: Component {
self.phoneView.play() self.phoneView.play()
} else if !isVisible { } else if !isVisible {
self.phoneView.reset() self.phoneView.reset()
self.fasterStarsView?.stopAnimation() self.decorationView?.resetAnimation()
} }
if let _ = transition.userData(DemoAnimateInTransition.self), abs(mappedPosition) < .ulpOfOne { if let _ = transition.userData(DemoAnimateInTransition.self), abs(mappedPosition) < .ulpOfOne {

View File

@ -725,7 +725,8 @@ private final class DemoSheetContent: CombinedComponent {
content: AnyComponent(PhoneDemoComponent( content: AnyComponent(PhoneDemoComponent(
context: component.context, context: component.context,
position: .bottom, position: .bottom,
videoFile: configuration.videos["more_upload"] videoFile: configuration.videos["more_upload"],
decoration: .dataRain
)), )),
title: strings.Premium_UploadSize, title: strings.Premium_UploadSize,
text: strings.Premium_UploadSizeInfo, text: strings.Premium_UploadSizeInfo,
@ -760,7 +761,8 @@ private final class DemoSheetContent: CombinedComponent {
content: AnyComponent(PhoneDemoComponent( content: AnyComponent(PhoneDemoComponent(
context: component.context, context: component.context,
position: .top, position: .top,
videoFile: configuration.videos["voice_to_text"] videoFile: configuration.videos["voice_to_text"],
decoration: .badgeStars
)), )),
title: strings.Premium_VoiceToText, title: strings.Premium_VoiceToText,
text: strings.Premium_VoiceToTextInfo, text: strings.Premium_VoiceToTextInfo,
@ -777,7 +779,8 @@ private final class DemoSheetContent: CombinedComponent {
content: AnyComponent(PhoneDemoComponent( content: AnyComponent(PhoneDemoComponent(
context: component.context, context: component.context,
position: .bottom, position: .bottom,
videoFile: configuration.videos["no_ads"] videoFile: configuration.videos["no_ads"],
decoration: .swirlStars
)), )),
title: strings.Premium_NoAds, title: strings.Premium_NoAds,
text: isStandalone ? strings.Premium_NoAdsStandaloneInfo : strings.Premium_NoAdsInfo, text: isStandalone ? strings.Premium_NoAdsStandaloneInfo : strings.Premium_NoAdsInfo,
@ -831,7 +834,8 @@ private final class DemoSheetContent: CombinedComponent {
content: AnyComponent(PhoneDemoComponent( content: AnyComponent(PhoneDemoComponent(
context: component.context, context: component.context,
position: .top, position: .top,
videoFile: configuration.videos["advanced_chat_management"] videoFile: configuration.videos["advanced_chat_management"],
decoration: .swirlStars
)), )),
title: strings.Premium_ChatManagement, title: strings.Premium_ChatManagement,
text: strings.Premium_ChatManagementInfo, text: strings.Premium_ChatManagementInfo,
@ -866,7 +870,8 @@ private final class DemoSheetContent: CombinedComponent {
content: AnyComponent(PhoneDemoComponent( content: AnyComponent(PhoneDemoComponent(
context: component.context, context: component.context,
position: .top, position: .top,
videoFile: configuration.videos["animated_userpics"] videoFile: configuration.videos["animated_userpics"],
decoration: .swirlStars
)), )),
title: strings.Premium_Avatar, title: strings.Premium_Avatar,
text: strings.Premium_AvatarInfo, text: strings.Premium_AvatarInfo,

View File

@ -1161,13 +1161,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
}) })
let termsString: MultilineTextComponent.TextContent let termsString: MultilineTextComponent.TextContent
if context.component.isPremium == true { if let promoConfiguration = context.state.promoConfiguration {
if let promoConfiguration = context.state.promoConfiguration { let attributedString = stringWithAppliedEntities(promoConfiguration.status, entities: promoConfiguration.statusEntities, baseColor: termsTextColor, linkColor: environment.theme.list.itemAccentColor, baseFont: termsFont, linkFont: termsFont, boldFont: boldTermsFont, italicFont: italicTermsFont, boldItalicFont: boldItalicTermsFont, fixedFont: monospaceTermsFont, blockQuoteFont: termsFont)
let attributedString = stringWithAppliedEntities(promoConfiguration.status, entities: promoConfiguration.statusEntities, baseColor: termsTextColor, linkColor: environment.theme.list.itemAccentColor, baseFont: termsFont, linkFont: termsFont, boldFont: boldTermsFont, italicFont: italicTermsFont, boldItalicFont: boldItalicTermsFont, fixedFont: monospaceTermsFont, blockQuoteFont: termsFont) termsString = .plain(attributedString)
termsString = .plain(attributedString)
} else {
termsString = .plain(NSAttributedString())
}
} else { } else {
termsString = .markdown( termsString = .markdown(
text: strings.Premium_Terms, text: strings.Premium_Terms,
@ -1231,6 +1227,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
size.height += 10.0 size.height += 10.0
size.height += scrollEnvironment.insets.bottom size.height += scrollEnvironment.insets.bottom
if context.component.source != .settings {
size.height += 44.0
}
return size return size
} }
} }
@ -1393,6 +1393,13 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
return return
} }
guard !self.context.account.testingEnvironment else {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let alertController = textAlertController(context: self.context, title: nil, text: "Telegram Premium is not available in the test environment.", actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
self.present(alertController)
return
}
addAppLogEvent(postbox: self.context.account.postbox, type: "premium.promo_screen_accept") addAppLogEvent(postbox: self.context.account.postbox, type: "premium.promo_screen_accept")
self.inProgress = true self.inProgress = true
@ -1403,7 +1410,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|> deliverOnMainQueue).start(next: { [weak self] available in |> deliverOnMainQueue).start(next: { [weak self] available in
if let strongSelf = self { if let strongSelf = self {
if available { if available {
strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct, account: strongSelf.context.account) strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct)
|> deliverOnMainQueue).start(next: { [weak self] status in |> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self, case .purchased = status { if let strongSelf = self, case .purchased = status {
strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId) strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId)
@ -1418,11 +1425,28 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|> mapToSignal { _ -> Signal<Never, AssignAppStoreTransactionError> in |> mapToSignal { _ -> Signal<Never, AssignAppStoreTransactionError> in
return .never() return .never()
} }
|> deliverOnMainQueue).start(error: { _ in |> timeout(15.0, queue: Queue.mainQueue(), alternate: .fail(.timeout))
|> deliverOnMainQueue).start(error: { [weak self] _ in
if let strongSelf = self {
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .immediate)
strongSelf.completion()
addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail")
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
let alertController = textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
strongSelf.present(alertController)
}
}, completed: { [weak self] in }, completed: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
let _ = updatePremiumPromoConfigurationOnce(account: strongSelf.context.account).start() let _ = updatePremiumPromoConfigurationOnce(account: strongSelf.context.account).start()
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.isPremium = true strongSelf.isPremium = true
strongSelf.updated(transition: .easeInOut(duration: 0.25)) strongSelf.updated(transition: .easeInOut(duration: 0.25))
strongSelf.completion() strongSelf.completion()
@ -1444,6 +1468,10 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
errorText = presentationData.strings.Premium_Purchase_ErrorNetwork errorText = presentationData.strings.Premium_Purchase_ErrorNetwork
case .notAllowed: case .notAllowed:
errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed
case .cantMakePayments:
errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments
case .assignFailed:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .cancelled: case .cancelled:
break break
} }

View File

@ -819,55 +819,56 @@ private final class LimitSheetContent: CombinedComponent {
reachedMaximumLimit = false reachedMaximumLimit = false
} }
let title = title.update( let contentSize: CGSize
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: titleText,
font: Font.semibold(17.0),
textColor: theme.actionSheet.primaryTextColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let textFont = Font.regular(17.0)
let boldTextFont = Font.semibold(17.0)
let textColor = theme.actionSheet.primaryTextColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in
return nil
})
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: string, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.0
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
let gradientColors: [UIColor]
if isPremiumDisabled {
gradientColors = [
UIColor(rgb: 0x007afe),
UIColor(rgb: 0x5494ff)
]
} else {
gradientColors = [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
]
}
if state.initialized { if state.initialized {
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: titleText,
font: Font.semibold(17.0),
textColor: theme.actionSheet.primaryTextColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let textFont = Font.regular(17.0)
let boldTextFont = Font.semibold(17.0)
let textColor = theme.actionSheet.primaryTextColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in
return nil
})
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: string, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.0
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
let gradientColors: [UIColor]
if isPremiumDisabled {
gradientColors = [
UIColor(rgb: 0x007afe),
UIColor(rgb: 0x5494ff)
]
} else {
gradientColors = [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
]
}
let limit = limit.update( let limit = limit.update(
component: PremiumLimitDisplayComponent( component: PremiumLimitDisplayComponent(
inactiveColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5), inactiveColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5),
@ -889,58 +890,60 @@ private final class LimitSheetContent: CombinedComponent {
context.add(limit context.add(limit
.position(CGPoint(x: context.availableSize.width / 2.0, y: limit.size.height / 2.0 + 44.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: limit.size.height / 2.0 + 44.0))
) )
}
let isIncreaseButton = !reachedMaximumLimit && !isPremiumDisabled let isIncreaseButton = !reachedMaximumLimit && !isPremiumDisabled
let button = button.update( let button = button.update(
component: SolidRoundedButtonComponent( component: SolidRoundedButtonComponent(
title: isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK, title: isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK,
theme: SolidRoundedButtonComponent.Theme( theme: SolidRoundedButtonComponent.Theme(
backgroundColor: .black, backgroundColor: .black,
backgroundColors: gradientColors, backgroundColors: gradientColors,
foregroundColor: .white foregroundColor: .white
),
font: .bold,
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
gloss: isIncreaseButton,
animationName: isIncreaseButton ? buttonAnimationName : nil,
iconPosition: .right,
action: { [weak component] in
guard let component = component else {
return
}
component.dismiss()
if isIncreaseButton {
component.action()
}
}
), ),
font: .bold, availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
fontSize: 17.0, transition: context.transition
height: 50.0, )
cornerRadius: 10.0,
gloss: isIncreaseButton, var textOffset: CGFloat = 228.0
animationName: isIncreaseButton ? buttonAnimationName : nil, if isPremiumDisabled {
iconPosition: .right, textOffset -= 68.0
action: { [weak component] in }
guard let component = component else {
return context.add(title
} .position(CGPoint(x: context.availableSize.width / 2.0, y: 28.0))
component.dismiss() )
if isIncreaseButton { context.add(text
component.action() .position(CGPoint(x: context.availableSize.width / 2.0, y: textOffset))
} )
}
), let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: textOffset + ceil(text.size.height / 2.0) + 38.0), size: button.size)
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), context.add(button
transition: context.transition .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
) )
var textOffset: CGFloat = 228.0 contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom)
if isPremiumDisabled { } else {
textOffset -= 68.0 contentSize = CGSize(width: context.availableSize.width, height: 351.0 + environment.safeInsets.bottom)
} }
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: 28.0))
)
context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: textOffset))
)
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: textOffset + ceil(text.size.height / 2.0) + 38.0), size: button.size)
context.add(button
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
)
let contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom)
return contentSize return contentSize
} }
} }

View File

@ -386,13 +386,13 @@ class PremiumStarComponent: Component {
animation.fromValue = NSValue(scnMatrix4: initial) animation.fromValue = NSValue(scnMatrix4: initial)
animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -1.6, 0.0, 0.0)) animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -1.6, 0.0, 0.0))
animation.timingFunction = CAMediaTimingFunction(name: .easeOut) animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
animation.beginTime = 0.6 animation.beginTime = 1.1
animation.duration = 0.9 animation.duration = 0.9
let group = CAAnimationGroup() let group = CAAnimationGroup()
group.animations = [animation] group.animations = [animation]
group.beginTime = 1.0 group.beginTime = 1.0
group.duration = 3.0 group.duration = 4.0
group.repeatCount = .infinity group.repeatCount = .infinity
node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer") node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer")

View File

@ -80,7 +80,7 @@ final class StickersCarouselComponent: Component {
} }
} }
private let itemSize = CGSize(width: 200.0, height: 200.0) private let itemSize = CGSize(width: 220.0, height: 220.0)
private class StickerNode: ASDisplayNode { private class StickerNode: ASDisplayNode {
private let context: AccountContext private let context: AccountContext

View File

@ -0,0 +1,204 @@
import Foundation
import UIKit
import SceneKit
import Display
import AppBundle
import SwiftSignalKit
final class SwirlStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
private var particles: SCNNode?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "swirl", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.alpha = 0.0
self.addSubview(self.sceneView)
self.particles = self.sceneView.scene?.rootNode.childNode(withName: "particles", recursively: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.particles = nil
}
func setVisible(_ visible: Bool) {
if visible, let particles = self.particles, particles.parent == nil {
self.sceneView.scene?.rootNode.addChildNode(particles)
}
self.setupAnimations()
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.6 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible && strongSelf.particles?.parent != nil {
strongSelf.particles?.removeFromParentNode()
if let node = strongSelf.sceneView.scene?.rootNode.childNode(withName: "star", recursively: false) {
node.removeAllAnimations()
}
}
})
}
func setupAnimations() {
guard let node = self.sceneView.scene?.rootNode.childNode(withName: "star", recursively: false), node.animationKeys.isEmpty else {
return
}
let initial = node.eulerAngles
let target = SCNVector3(x: node.eulerAngles.x + .pi * 2.0, y: node.eulerAngles.y, z: node.eulerAngles.z)
let animation = CABasicAnimation(keyPath: "eulerAngles")
animation.fromValue = NSValue(scnVector3: initial)
animation.toValue = NSValue(scnVector3: target)
animation.duration = 1.5
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.fillMode = .forwards
animation.repeatCount = .infinity
node.addAnimation(animation, forKey: "rotation")
self.setupMovementAnimation()
}
func setupMovementAnimation() {
guard let node = self.sceneView.scene?.rootNode.childNode(withName: "star", recursively: false) else {
return
}
node.position = SCNVector3(3.5, 0.0, -2.0)
let firstPath = UIBezierPath()
firstPath.move(to: CGPoint(x: 3.5, y: -2.0))
firstPath.addLine(to: CGPoint(x: -15.5, y: 15.5))
let firstAction = SCNAction.moveAlong(path: firstPath, duration: 2.0)
SCNTransaction.begin()
SCNTransaction.animationDuration = 2.0
node.runAction(firstAction)
SCNTransaction.completionBlock = { [weak self, weak node] in
Queue.mainQueue().after(2.2, {
node?.position = SCNVector3(0.0, 0.0, -3.0)
let secondPath = UIBezierPath()
secondPath.move(to: CGPoint(x: 0.0, y: -3.0))
secondPath.addLine(to: CGPoint(x: 15.5, y: 20.0))
let secondAction = SCNAction.moveAlong(path: secondPath, duration: 2.0)
SCNTransaction.begin()
SCNTransaction.animationDuration = 2.0
node?.runAction(secondAction)
SCNTransaction.completionBlock = { [weak self] in
Queue.mainQueue().after(2.2, {
self?.setupMovementAnimation()
})
}
SCNTransaction.commit()
})
}
SCNTransaction.commit()
}
func resetAnimation() {
}
override func layoutSubviews() {
super.layoutSubviews()
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
}
}
extension UIBezierPath {
var elements: [PathElement] {
var pathElements = [PathElement]()
withUnsafeMutablePointer(to: &pathElements) { elementsPointer in
cgPath.apply(info: elementsPointer) { (userInfo, nextElementPointer) in
let nextElement = PathElement(element: nextElementPointer.pointee)
let elementsPointer = userInfo!.assumingMemoryBound(to: [PathElement].self)
elementsPointer.pointee.append(nextElement)
}
}
return pathElements
}
}
enum PathElement {
case moveToPoint(CGPoint)
case addLineToPoint(CGPoint)
case addQuadCurveToPoint(CGPoint, CGPoint)
case addCurveToPoint(CGPoint, CGPoint, CGPoint)
case closeSubpath
init(element: CGPathElement) {
switch element.type {
case .moveToPoint:
self = .moveToPoint(element.points[0])
case .addLineToPoint:
self = .addLineToPoint(element.points[0])
case .addQuadCurveToPoint:
self = .addQuadCurveToPoint(element.points[0], element.points[1])
case .addCurveToPoint:
self = .addCurveToPoint(element.points[0], element.points[1], element.points[2])
case .closeSubpath:
self = .closeSubpath
@unknown default:
self = .closeSubpath
}
}
}
public extension SCNAction {
class func moveAlong(path: UIBezierPath, duration animationDuration: Double) -> SCNAction {
let points = path.elements
var actions = [SCNAction]()
for point in points {
switch point {
case .moveToPoint(let a):
let moveAction = SCNAction.move(to: SCNVector3(a.x, 0, a.y), duration: animationDuration)
actions.append(moveAction)
break
case .addCurveToPoint(let a, let b, let c):
let moveAction1 = SCNAction.move(to: SCNVector3(a.x, 0, a.y), duration: animationDuration)
let moveAction2 = SCNAction.move(to: SCNVector3(b.x, 0, b.y), duration: animationDuration)
let moveAction3 = SCNAction.move(to: SCNVector3(c.x, 0, c.y), duration: animationDuration)
actions.append(moveAction1)
actions.append(moveAction2)
actions.append(moveAction3)
break
case .addLineToPoint(let a):
let moveAction = SCNAction.move(to: SCNVector3(a.x, 0, a.y), duration: animationDuration)
actions.append(moveAction)
break
case .addQuadCurveToPoint(let a, let b):
let moveAction1 = SCNAction.move(to: SCNVector3(a.x, 0, a.y), duration: animationDuration)
let moveAction2 = SCNAction.move(to: SCNVector3(b.x, 0, b.y), duration: animationDuration)
actions.append(moveAction1)
actions.append(moveAction2)
break
default:
let moveAction = SCNAction.move(to: SCNVector3(0, 0, 0), duration: animationDuration)
actions.append(moveAction)
break
}
}
return SCNAction.sequence(actions)
}
}

View File

@ -323,46 +323,82 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
} }
} }
private func generatePremiumReactionIcon() -> UIImage? { private let starsCount = 7
return generateImage(CGSize(width: 32.0, height: 32.0), contextGenerator: { size, context in private final class StarsNode: ASDisplayNode {
context.clear(CGRect(origin: CGPoint(), size: size)) private let starNodes: [ASImageNode]
if let backgroundImage = UIImage(bundleImageName: "Premium/BackgroundIcon"), let foregroundImage = UIImage(bundleImageName: "Premium/ForegroundIcon") { private var timer: SwiftSignalKit.Timer?
context.saveGState()
if let cgImage = backgroundImage.cgImage { override init() {
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) let image = UIImage(bundleImageName: "Premium/ReactionsStar")
} var starNodes: [ASImageNode] = []
for _ in 0 ..< starsCount {
let colorsArray: [CGColor] = [ let node = ASImageNode()
UIColor(rgb: 0x6B93FF).cgColor, node.alpha = 0.0
UIColor(rgb: 0x6B93FF).cgColor, node.image = image
UIColor(rgb: 0x976FFF).cgColor, node.displaysAsynchronously = false
UIColor(rgb: 0xE46ACE).cgColor, starNodes.append(node)
UIColor(rgb: 0xE46ACE).cgColor
]
var locations: [CGFloat] = [0.0, 0.15, 0.5, 0.85, 1.0]
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions())
context.restoreGState()
if let cgImage = foregroundImage.cgImage {
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
}
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
} }
}) self.starNodes = starNodes
super.init()
for node in starNodes {
self.addSubnode(node)
}
self.setup(firstTime: true)
self.timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
self?.setup()
}, queue: Queue.mainQueue())
self.timer?.start()
}
deinit {
self.timer?.invalidate()
}
func setup(firstTime: Bool = false) {
let size = CGSize(width: 32.0, height: 32.0)
let starSize = CGSize(width: 6.0, height: 8.0)
for node in self.starNodes {
if node.layer.animation(forKey: "transform.scale") == nil && node.layer.animation(forKey: "opacity") == nil {
let x = CGFloat.random(in: 0 ..< size.width)
let y = CGFloat.random(in: 0 ..< size.width)
let randomTargetScale = CGFloat.random(in: 0.8 ..< 1.0)
node.bounds = CGRect(origin: .zero, size: starSize)
node.position = CGPoint(x: x, y: y)
node.alpha = 1.0
let duration = CGFloat.random(in: 0.4 ..< 0.65)
let delay = firstTime ? CGFloat.random(in: 0.0 ..< 0.25) : 0.0
node.layer.animateScale(from: 0.001, to: randomTargetScale, duration: duration, delay: delay, removeOnCompletion: false, completion: { [weak self, weak node] _ in
let duration = CGFloat.random(in: 0.3 ..< 0.35)
node?.alpha = 0.0
node?.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak self, weak node] _ in
node?.layer.removeAllAnimations()
self?.setup()
})
})
}
}
}
} }
final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode { final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode {
var isExtracted: Bool = false var isExtracted: Bool = false
var backgroundView: UIVisualEffectView? private var backgroundView: UIVisualEffectView?
let backgroundMaskNode: ASImageNode private let backgroundMaskNode: ASImageNode
let backgroundOverlayNode: ASImageNode private let backgroundOverlayNode: ASImageNode
let imageNode: ASImageNode private let imageNode: ASImageNode
let maskImageNode: ASImageNode private var starsNode: StarsNode?
private let maskContainerNode: ASDisplayNode
private let maskImageNode: ASImageNode
init(theme: PresentationTheme) { init(theme: PresentationTheme) {
self.backgroundMaskNode = ASImageNode() self.backgroundMaskNode = ASImageNode()
@ -372,7 +408,7 @@ final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode {
self.backgroundMaskNode.image = UIImage(bundleImageName: "Premium/ReactionsBackground") self.backgroundMaskNode.image = UIImage(bundleImageName: "Premium/ReactionsBackground")
self.backgroundOverlayNode = ASImageNode() self.backgroundOverlayNode = ASImageNode()
self.backgroundOverlayNode.alpha = 0.05 self.backgroundOverlayNode.alpha = 0.1
self.backgroundOverlayNode.contentMode = .center self.backgroundOverlayNode.contentMode = .center
self.backgroundOverlayNode.displaysAsynchronously = false self.backgroundOverlayNode.displaysAsynchronously = false
self.backgroundOverlayNode.isUserInteractionEnabled = false self.backgroundOverlayNode.isUserInteractionEnabled = false
@ -384,20 +420,24 @@ final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode {
self.imageNode.isUserInteractionEnabled = false self.imageNode.isUserInteractionEnabled = false
self.imageNode.image = UIImage(bundleImageName: "Premium/ReactionsForeground") self.imageNode.image = UIImage(bundleImageName: "Premium/ReactionsForeground")
self.maskContainerNode = ASDisplayNode()
self.maskImageNode = ASImageNode() self.maskImageNode = ASImageNode()
if let backgroundImage = UIImage(bundleImageName: "Premium/ReactionsBackground") { if let backgroundImage = UIImage(bundleImageName: "Premium/ReactionsBackground") {
self.maskImageNode.image = generateImage(CGSize(width: 40.0, height: 52.0), contextGenerator: { size, context in self.maskImageNode.image = generateImage(CGSize(width: 40.0 * 4.0, height: 52.0 * 4.0), contextGenerator: { size, context in
context.setFillColor(UIColor.black.cgColor) context.setFillColor(UIColor.black.cgColor)
context.fill(CGRect(origin: .zero, size: size)) context.fill(CGRect(origin: .zero, size: size))
if let cgImage = backgroundImage.cgImage { if let cgImage = backgroundImage.cgImage {
let maskFrame = CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 10.0) let maskFrame = CGRect(origin: .zero, size: size).insetBy(dx: 4.0 + 40.0 * 2.0 - 16.0, dy: 10.0 + 52.0 * 2.0 - 16.0)
context.clip(to: maskFrame, mask: cgImage) context.clip(to: maskFrame, mask: cgImage)
} }
context.setBlendMode(.clear) context.setBlendMode(.clear)
context.fill(CGRect(origin: .zero, size: size)) context.fill(CGRect(origin: .zero, size: size))
}) })
} }
self.maskImageNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((40.0 - 40.0 * 4.0) / 2.0), y: floorToScreenPixels((52.0 - 52.0 * 4.0) / 2.0)), size: CGSize(width: 40.0 * 4.0, height: 52.0 * 4.0))
self.maskContainerNode.addSubnode(self.maskImageNode)
super.init() super.init()
@ -418,10 +458,37 @@ final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode {
backgroundView.mask = self.backgroundMaskNode.view backgroundView.mask = self.backgroundMaskNode.view
self.view.insertSubview(backgroundView, at: 0) self.view.insertSubview(backgroundView, at: 0)
self.backgroundView = backgroundView self.backgroundView = backgroundView
let starsNode = StarsNode()
starsNode.frame = CGRect(origin: .zero, size: CGSize(width: 32.0, height: 32.0))
self.backgroundView?.contentView.addSubview(starsNode.view)
self.starsNode = starsNode
} }
func appear(animated: Bool) { func appear(animated: Bool) {
if animated {
let delay: Double = 0.1
let duration: Double = 0.85
let damping: CGFloat = 60.0
let initialScale: CGFloat = 0.25
self.maskImageNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, delay: delay, damping: damping)
self.backgroundView?.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, delay: delay, damping: damping)
self.backgroundOverlayNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, delay: delay, damping: damping)
self.imageNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, delay: delay, damping: damping)
Queue.mainQueue().after(0.25, {
let shimmerNode = ASImageNode()
shimmerNode.displaysAsynchronously = false
shimmerNode.image = generateGradientImage(size: CGSize(width: 32.0, height: 32.0), colors: [UIColor(rgb: 0xffffff, alpha: 0.0), UIColor(rgb: 0xffffff, alpha: 0.24), UIColor(rgb: 0xffffff, alpha: 0.0)], locations: [0.0, 0.5, 1.0], direction: .horizontal)
shimmerNode.frame = CGRect(origin: .zero, size: CGSize(width: 32.0, height: 32.0))
self.backgroundView?.contentView.addSubview(shimmerNode.view)
shimmerNode.layer.animatePosition(from: CGPoint(x: -60.0, y: 0.0), to: CGPoint(x: 60.0, y: 0.0), duration: 0.75, removeOnCompletion: false, additive: true, completion: { [weak shimmerNode] _ in
shimmerNode?.view.removeFromSuperview()
})
})
}
} }
func updateLayout(size: CGSize, isExpanded: Bool, largeExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition) { func updateLayout(size: CGSize, isExpanded: Bool, largeExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition) {
@ -432,8 +499,7 @@ final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode {
self.imageNode.frame = bounds self.imageNode.frame = bounds
} }
var maskNode: ASDisplayNode? { var maskNode: ASDisplayNode? {
return self.maskImageNode return self.maskContainerNode
} }
} }

View File

@ -118,6 +118,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
let animationNode = SimpleAnimationNode(animationName: animation, size: CGSize(width: 30.0, height: 30.0)) let animationNode = SimpleAnimationNode(animationName: animation, size: CGSize(width: 30.0, height: 30.0))
animationNode.customColor = self.theme.foregroundColor animationNode.customColor = self.theme.foregroundColor
animationNode.isUserInteractionEnabled = false
self.addSubnode(animationNode) self.addSubnode(animationNode)
self.animationNode = animationNode self.animationNode = animationNode
@ -128,10 +129,10 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
if self.gloss { if self.gloss {
self.animationTimer?.invalidate() self.animationTimer?.invalidate()
Queue.mainQueue().after(0.75) { Queue.mainQueue().after(1.25) {
self.animationNode?.play() self.animationNode?.play()
let timer = SwiftSignalKit.Timer(timeout: 3.0, repeat: true, completion: { [weak self] in let timer = SwiftSignalKit.Timer(timeout: 4.0, repeat: true, completion: { [weak self] in
self?.animationNode?.play() self?.animationNode?.play()
}, queue: Queue.mainQueue()) }, queue: Queue.mainQueue())
self.animationTimer = timer self.animationTimer = timer
@ -227,6 +228,8 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
strongSelf.subtitleNode.alpha = 0.55 strongSelf.subtitleNode.alpha = 0.55
strongSelf.iconNode.layer.removeAnimation(forKey: "opacity") strongSelf.iconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.iconNode.alpha = 0.55 strongSelf.iconNode.alpha = 0.55
strongSelf.animationNode?.layer.removeAnimation(forKey: "opacity")
strongSelf.animationNode?.alpha = 0.55
} else { } else {
if strongSelf.buttonBackgroundNode.alpha > 0.0 { if strongSelf.buttonBackgroundNode.alpha > 0.0 {
strongSelf.buttonBackgroundNode.alpha = 1.0 strongSelf.buttonBackgroundNode.alpha = 1.0
@ -237,6 +240,8 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
strongSelf.subtitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) strongSelf.subtitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
strongSelf.iconNode.alpha = 1.0 strongSelf.iconNode.alpha = 1.0
strongSelf.iconNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) strongSelf.iconNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
strongSelf.animationNode?.alpha = 1.0
strongSelf.animationNode?.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
} }
} }
} }
@ -358,8 +363,8 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
compositingFilter = nil compositingFilter = nil
} }
shimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(alpha), gradientSize: 70.0, globalTimeOffset: false, duration: 3.0, horizontal: true) shimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(alpha), gradientSize: 70.0, globalTimeOffset: false, duration: 4.0, horizontal: true)
borderShimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(borderAlpha), gradientSize: 70.0, globalTimeOffset: false, duration: 3.0, horizontal: true) borderShimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(borderAlpha), gradientSize: 70.0, globalTimeOffset: false, duration: 4.0, horizontal: true)
shimmerView.layer.compositingFilter = compositingFilter shimmerView.layer.compositingFilter = compositingFilter
borderShimmerView.layer.compositingFilter = compositingFilter borderShimmerView.layer.compositingFilter = compositingFilter
@ -662,6 +667,7 @@ public final class SolidRoundedButtonView: UIView {
let animationNode = SimpleAnimationNode(animationName: animation, size: CGSize(width: 30.0, height: 30.0)) let animationNode = SimpleAnimationNode(animationName: animation, size: CGSize(width: 30.0, height: 30.0))
animationNode.customColor = self.theme.foregroundColor animationNode.customColor = self.theme.foregroundColor
animationNode.isUserInteractionEnabled = false
self.addSubview(animationNode.view) self.addSubview(animationNode.view)
self.animationNode = animationNode self.animationNode = animationNode
@ -672,10 +678,10 @@ public final class SolidRoundedButtonView: UIView {
if self.gloss { if self.gloss {
self.animationTimer?.invalidate() self.animationTimer?.invalidate()
Queue.mainQueue().after(0.75) { Queue.mainQueue().after(1.25) {
self.animationNode?.play() self.animationNode?.play()
let timer = SwiftSignalKit.Timer(timeout: 3.0, repeat: true, completion: { [weak self] in let timer = SwiftSignalKit.Timer(timeout: 4.0, repeat: true, completion: { [weak self] in
self?.animationNode?.play() self?.animationNode?.play()
}, queue: Queue.mainQueue()) }, queue: Queue.mainQueue())
self.animationTimer = timer self.animationTimer = timer
@ -784,6 +790,8 @@ public final class SolidRoundedButtonView: UIView {
strongSelf.subtitleNode.alpha = 0.55 strongSelf.subtitleNode.alpha = 0.55
strongSelf.iconNode.layer.removeAnimation(forKey: "opacity") strongSelf.iconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.iconNode.alpha = 0.55 strongSelf.iconNode.alpha = 0.55
strongSelf.animationNode?.layer.removeAnimation(forKey: "opacity")
strongSelf.animationNode?.alpha = 0.55
} else { } else {
if strongSelf.buttonBackgroundNode.alpha > 0.0 { if strongSelf.buttonBackgroundNode.alpha > 0.0 {
strongSelf.buttonBackgroundNode.alpha = 1.0 strongSelf.buttonBackgroundNode.alpha = 1.0
@ -794,6 +802,8 @@ public final class SolidRoundedButtonView: UIView {
strongSelf.subtitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) strongSelf.subtitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
strongSelf.iconNode.alpha = 1.0 strongSelf.iconNode.alpha = 1.0
strongSelf.iconNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) strongSelf.iconNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
strongSelf.animationNode?.alpha = 1.0
strongSelf.animationNode?.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
} }
} }
} }
@ -808,6 +818,10 @@ public final class SolidRoundedButtonView: UIView {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
deinit {
self.animationTimer?.invalidate()
}
private func setupGloss() { private func setupGloss() {
if self.gloss { if self.gloss {
if self.shimmerView == nil { if self.shimmerView == nil {
@ -1002,10 +1016,10 @@ public final class SolidRoundedButtonView: UIView {
compositingFilter = nil compositingFilter = nil
} }
let globalTimeOffset = self.icon == nil let globalTimeOffset = self.icon == nil && self.animation == nil
shimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(alpha), gradientSize: 70.0, globalTimeOffset: globalTimeOffset, duration: 3.0, horizontal: true) shimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(alpha), gradientSize: 70.0, globalTimeOffset: globalTimeOffset, duration: 4.0, horizontal: true)
borderShimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(borderAlpha), gradientSize: 70.0, globalTimeOffset: globalTimeOffset, duration: 3.0, horizontal: true) borderShimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(borderAlpha), gradientSize: 70.0, globalTimeOffset: globalTimeOffset, duration: 4.0, horizontal: true)
shimmerView.layer.compositingFilter = compositingFilter shimmerView.layer.compositingFilter = compositingFilter
borderShimmerView.layer.compositingFilter = compositingFilter borderShimmerView.layer.compositingFilter = compositingFilter

View File

@ -6,6 +6,7 @@ import TelegramApi
public enum AssignAppStoreTransactionError { public enum AssignAppStoreTransactionError {
case generic case generic
case timeout
} }
func _internal_sendAppStoreReceipt(account: Account, receipt: Data, restore: Bool) -> Signal<Never, AssignAppStoreTransactionError> { func _internal_sendAppStoreReceipt(account: Account, receipt: Data, restore: Bool) -> Signal<Never, AssignAppStoreTransactionError> {

View File

@ -2,7 +2,6 @@ import Foundation
import Postbox import Postbox
import SwiftSignalKit import SwiftSignalKit
public enum TogglePeerChatPinnedLocation { public enum TogglePeerChatPinnedLocation {
case group(PeerGroupId) case group(PeerGroupId)
case filter(Int32) case filter(Int32)
@ -17,6 +16,10 @@ func _internal_toggleItemPinned(postbox: Postbox, accountPeerId: PeerId, locatio
return postbox.transaction { transaction -> TogglePeerChatPinnedResult in return postbox.transaction { transaction -> TogglePeerChatPinnedResult in
let isPremium = transaction.getPeer(accountPeerId)?.isPremium ?? false let isPremium = transaction.getPeer(accountPeerId)?.isPremium ?? false
let appConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? .defaultValue
let limitsConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.limitsConfiguration)?.get(LimitsConfiguration.self) ?? LimitsConfiguration.defaultValue
let userLimitsConfiguration = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: isPremium)
switch location { switch location {
case let .group(groupId): case let .group(groupId):
var itemIds = transaction.getPinnedItemIds(groupId: groupId) var itemIds = transaction.getPinnedItemIds(groupId: groupId)
@ -39,9 +42,8 @@ func _internal_toggleItemPinned(postbox: Postbox, accountPeerId: PeerId, locatio
additionalCount = 1 additionalCount = 1
} }
let appConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? .defaultValue
let limitsConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.limitsConfiguration)?.get(LimitsConfiguration.self) ?? LimitsConfiguration.defaultValue
let userLimitsConfiguration = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: isPremium)
let limitCount: Int let limitCount: Int
if case .root = groupId { if case .root = groupId {
@ -75,7 +77,7 @@ func _internal_toggleItemPinned(postbox: Postbox, accountPeerId: PeerId, locatio
updatedData.includePeers.removePinnedPeer(peerId) updatedData.includePeers.removePinnedPeer(peerId)
} else { } else {
if !updatedData.includePeers.addPinnedPeer(peerId) { if !updatedData.includePeers.addPinnedPeer(peerId) {
result = .limitExceeded(count: updatedData.includePeers.pinnedPeers.count, limit: 100) result = .limitExceeded(count: updatedData.includePeers.peers.count, limit: Int(userLimitsConfiguration.maxFolderChatsCount))
} }
} }
filters[index] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) filters[index] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData)

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ministar@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

View File

@ -96,7 +96,7 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
if let photo = cachedPeerData.photo, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) { if let photo = cachedPeerData.photo, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) {
let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value()
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])]))
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false) let videoContent = NativeVideoContent(id: .profileVideo(videoId, "header"), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false)
if videoContent.id != strongSelf.videoContent?.id { if videoContent.id != strongSelf.videoContent?.id {
strongSelf.videoNode?.removeFromSupernode() strongSelf.videoNode?.removeFromSupernode()
strongSelf.videoContent = videoContent strongSelf.videoContent = videoContent

View File

@ -3602,7 +3602,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return true return true
} }
self.chatTitleView = ChatTitleView(account: self.context.account, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder) self.chatTitleView = ChatTitleView(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder)
self.navigationItem.titleView = self.chatTitleView self.navigationItem.titleView = self.chatTitleView
self.chatTitleView?.pressed = { [weak self] in self.chatTitleView?.pressed = { [weak self] in
if let strongSelf = self { if let strongSelf = self {

View File

@ -483,7 +483,7 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode {
if let photo = cachedPeerData.photo, let video = photo.videoRepresentations.last, let peerReference = PeerReference(peer) { if let photo = cachedPeerData.photo, let video = photo.videoRepresentations.last, let peerReference = PeerReference(peer) {
let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value()
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])]))
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false) let videoContent = NativeVideoContent(id: .profileVideo(videoId, "\(Int32.random(in: 0 ..< Int32.max))"), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false)
if videoContent.id != strongSelf.videoContent?.id { if videoContent.id != strongSelf.videoContent?.id {
strongSelf.videoNode?.removeFromSupernode() strongSelf.videoNode?.removeFromSupernode()
strongSelf.videoContent = videoContent strongSelf.videoContent = videoContent

View File

@ -16,6 +16,7 @@ import LocalizedPeerData
import PhoneNumberFormat import PhoneNumberFormat
import ChatTitleActivityNode import ChatTitleActivityNode
import AnimatedCountLabelNode import AnimatedCountLabelNode
import AccountContext
private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]) private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers])
private let subtitleFont = Font.regular(13.0) private let subtitleFont = Font.regular(13.0)
@ -46,7 +47,7 @@ private enum ChatTitleCredibilityIcon {
} }
final class ChatTitleView: UIView, NavigationBarTitleView { final class ChatTitleView: UIView, NavigationBarTitleView {
private let account: Account private let context: AccountContext
private var theme: PresentationTheme private var theme: PresentationTheme
private var hasEmbeddedTitleContent: Bool = false private var hasEmbeddedTitleContent: Bool = false
@ -122,7 +123,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
segments = [.text(0, NSAttributedString(string: typeText, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] segments = [.text(0, NSAttributedString(string: typeText, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
isEnabled = false isEnabled = false
} else if isScheduledMessages { } else if isScheduledMessages {
if peerView.peerId == self.account.peerId { if peerView.peerId == self.context.account.peerId {
segments = [.text(0, NSAttributedString(string: self.strings.ScheduledMessages_RemindersTitle, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] segments = [.text(0, NSAttributedString(string: self.strings.ScheduledMessages_RemindersTitle, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
} else { } else {
segments = [.text(0, NSAttributedString(string: self.strings.ScheduledMessages_Title, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] segments = [.text(0, NSAttributedString(string: self.strings.ScheduledMessages_Title, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
@ -130,7 +131,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
isEnabled = false isEnabled = false
} else { } else {
if let peer = peerViewMainPeer(peerView) { if let peer = peerViewMainPeer(peerView) {
if peerView.peerId == self.account.peerId { if peerView.peerId == self.context.account.peerId {
segments = [.text(0, NSAttributedString(string: self.strings.Conversation_SavedMessages, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] segments = [.text(0, NSAttributedString(string: self.strings.Conversation_SavedMessages, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
} else { } else {
if !peerView.peerIsContact, let user = peer as? TelegramUser, !user.flags.contains(.isSupport), user.botInfo == nil, let phone = user.phone, !phone.isEmpty { if !peerView.peerIsContact, let user = peer as? TelegramUser, !user.flags.contains(.isSupport), user.botInfo == nil, let phone = user.phone, !phone.isEmpty {
@ -139,14 +140,15 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
segments = [.text(0, NSAttributedString(string: EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] segments = [.text(0, NSAttributedString(string: EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
} }
} }
if peer.id != self.account.peerId { if peer.id != self.context.account.peerId {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
if peer.isFake { if peer.isFake {
titleCredibilityIcon = .fake titleCredibilityIcon = .fake
} else if peer.isScam { } else if peer.isScam {
titleCredibilityIcon = .scam titleCredibilityIcon = .scam
} else if peer.isVerified { } else if peer.isVerified {
titleCredibilityIcon = .verified titleCredibilityIcon = .verified
} else if peer.isPremium { } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled {
titleCredibilityIcon = .premium titleCredibilityIcon = .premium
} }
} }
@ -303,7 +305,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
switch titleContent { switch titleContent {
case let .peer(peerView, _, isScheduledMessages): case let .peer(peerView, _, isScheduledMessages):
if let peer = peerViewMainPeer(peerView) { if let peer = peerViewMainPeer(peerView) {
if peer.id == self.account.peerId || isScheduledMessages || peer.id.isReplies { if peer.id == self.context.account.peerId || isScheduledMessages || peer.id.isReplies {
inputActivitiesAllowed = false inputActivitiesAllowed = false
} }
} }
@ -405,7 +407,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
case let .peer(peerView, onlineMemberCount, isScheduledMessages): case let .peer(peerView, onlineMemberCount, isScheduledMessages):
if let peer = peerViewMainPeer(peerView) { if let peer = peerViewMainPeer(peerView) {
let servicePeer = isServicePeer(peer) let servicePeer = isServicePeer(peer)
if peer.id == self.account.peerId || isScheduledMessages || peer.id.isReplies { if peer.id == self.context.account.peerId || isScheduledMessages || peer.id.isReplies {
let string = NSAttributedString(string: "", font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) let string = NSAttributedString(string: "", font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
state = .info(string, .generic) state = .info(string, .generic)
} else if let user = peer as? TelegramUser { } else if let user = peer as? TelegramUser {
@ -541,13 +543,13 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
} }
} }
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder) { init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder) {
self.account = account self.context = context
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.dateTimeFormat = dateTimeFormat self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder self.nameDisplayOrder = nameDisplayOrder
self.contentContainer = ASDisplayNode() self.contentContainer = ASDisplayNode()
self.titleNode = ImmediateAnimatedCountLabelNode() self.titleNode = ImmediateAnimatedCountLabelNode()

View File

@ -763,6 +763,21 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
} }
} else if parsedUrl.host == "premium_offer" { } else if parsedUrl.host == "premium_offer" {
handleResolvedUrl(.premiumOffer(reference: nil)) handleResolvedUrl(.premiumOffer(reference: nil))
} else if parsedUrl.host == "restore_purchases" {
context.inAppPurchaseManager?.restorePurchases(completion: { result in
let text: String
switch result {
case .succeed:
text = presentationData.strings.Premium_Restore_Success
case .failed:
text = presentationData.strings.Premium_Restore_ErrorUnknown
}
context.sharedContext.presentGlobalController(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
}),
], parseMarkdown: true), nil)
})
} }
} }

View File

@ -2310,7 +2310,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
credibilityIcon = .scam credibilityIcon = .scam
} else if peer.isVerified { } else if peer.isVerified {
credibilityIcon = .verified credibilityIcon = .verified
} else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled && (peer.id != self.context.account.peerId || self.isSettings) {
credibilityIcon = .premium credibilityIcon = .premium
} else { } else {
credibilityIcon = .none credibilityIcon = .none

View File

@ -842,7 +842,23 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat
items[.account]!.append(PeerInfoScreenActionItem(id: ItemAddAccount, text: presentationData.strings.Settings_AddAnotherAccount, alignment: .center, action: { items[.account]!.append(PeerInfoScreenActionItem(id: ItemAddAccount, text: presentationData.strings.Settings_AddAnotherAccount, alignment: .center, action: {
interaction.openSettings(.addAccount) interaction.openSettings(.addAccount)
})) }))
items[.account]!.append(PeerInfoScreenCommentItem(id: ItemAddAccountHelp, text: presentationData.strings.Settings_AddAnotherAccount_Help))
var hasPremiumAccounts = false
if data.peer?.isPremium == true && !context.account.testingEnvironment {
hasPremiumAccounts = true
}
if let settings = data.globalSettings {
for (accountContext, peer, _) in settings.accountsAndPeers {
if !accountContext.account.testingEnvironment {
if peer.isPremium {
hasPremiumAccounts = true
break
}
}
}
}
items[.account]!.append(PeerInfoScreenCommentItem(id: ItemAddAccountHelp, text: hasPremiumAccounts ? presentationData.strings.Settings_AddAnotherAccount_PremiumHelp : presentationData.strings.Settings_AddAnotherAccount_Help))
items[.logout]!.append(PeerInfoScreenActionItem(id: ItemLogout, text: presentationData.strings.Settings_Logout, color: .destructive, alignment: .center, action: { items[.logout]!.append(PeerInfoScreenActionItem(id: ItemLogout, text: presentationData.strings.Settings_Logout, color: .destructive, alignment: .center, action: {
interaction.openSettings(.logout) interaction.openSettings(.logout)
@ -6275,13 +6291,19 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
case .username: case .username:
push(usernameSetupController(context: self.context)) push(usernameSetupController(context: self.context))
case .addAccount: case .addAccount:
var maximumAvailableAccounts: Int = 3 let _ = (activeAccountsAndPeers(context: context)
if self.data?.peer?.isPremium == true && !self.context.account.testingEnvironment { |> take(1)
maximumAvailableAccounts = 4 |> deliverOnMainQueue
} ).start(next: { [weak self] accountAndPeer, accountsAndPeers in
var count: Int = 1 guard let strongSelf = self else {
if let settings = self.data?.globalSettings { return
for (accountContext, peer, _) in settings.accountsAndPeers { }
var maximumAvailableAccounts: Int = 3
if accountAndPeer?.1.isPremium == true && !strongSelf.context.account.testingEnvironment {
maximumAvailableAccounts = 4
}
var count: Int = 1
for (accountContext, peer, _) in accountsAndPeers {
if !accountContext.account.testingEnvironment { if !accountContext.account.testingEnvironment {
if peer.isPremium { if peer.isPremium {
maximumAvailableAccounts = 4 maximumAvailableAccounts = 4
@ -6289,23 +6311,23 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
count += 1 count += 1
} }
} }
}
if count >= maximumAvailableAccounts { if count >= maximumAvailableAccounts {
let context = self.context var replaceImpl: ((ViewController) -> Void)?
var replaceImpl: ((ViewController) -> Void)? let controller = PremiumLimitScreen(context: strongSelf.context, subject: .accounts, count: Int32(count), action: {
let controller = PremiumLimitScreen(context: context, subject: .accounts, count: Int32(count), action: { let controller = PremiumIntroScreen(context: strongSelf.context, source: .accounts)
let controller = PremiumIntroScreen(context: context, source: .accounts) replaceImpl?(controller)
replaceImpl?(controller) })
}) replaceImpl = { [weak controller] c in
replaceImpl = { [weak controller] c in controller?.replace(with: c)
controller?.replace(with: c) }
if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController {
navigationController.pushViewController(controller)
}
} else {
strongSelf.context.sharedContext.beginNewAuth(testingEnvironment: strongSelf.context.account.testingEnvironment)
} }
if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController { })
navigationController.pushViewController(controller)
}
} else {
self.context.sharedContext.beginNewAuth(testingEnvironment: self.context.account.testingEnvironment)
}
case .logout: case .logout:
if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone { if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone {
if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController { if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController {