mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge commit '3481e44afacbac6bb69023ede58fb0d166a889de'
This commit is contained in:
commit
b2477bc773
@ -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.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.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_any" = "Leave %@ Communities";
|
||||
|
||||
@ -7711,5 +7720,12 @@ Sorry for the inconvenience.";
|
||||
"Premium.Purchase.ErrorUnknown" = "An error occurred. Please 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.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.AddAnotherAccount.PremiumHelp" = "You can add up to four accounts with different phone numbers.";
|
||||
|
@ -354,14 +354,26 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
|
||||
case let .limitExceeded(count, _):
|
||||
f(.default)
|
||||
|
||||
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)
|
||||
if case .filter = location {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, 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)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -276,6 +276,23 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
|
||||
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 {
|
||||
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 {
|
||||
@ -306,24 +323,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
|
||||
})
|
||||
}
|
||||
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 _ = labelApply()
|
||||
|
||||
|
@ -294,7 +294,7 @@ private final class ItemNode: ASDisplayNode {
|
||||
if let deleteButtonNode = self.deleteButtonNode {
|
||||
if let theme = self.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
|
||||
}
|
||||
|
||||
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] = []
|
||||
for (id, _) in self.itemNodes {
|
||||
|
@ -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))
|
||||
|
||||
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))
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
var mediaPreviewOffsetX = textFrame.origin.x + 1.0
|
||||
var mediaPreviewOffsetX = textFrame.origin.x
|
||||
let contentImageSpacing: CGFloat = 2.0
|
||||
for (_, media, mediaSize) in self.currentMediaPreviewSpecs {
|
||||
guard let mediaId = media.id else {
|
||||
@ -2279,7 +2279,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
let badgeFrame = self.badgeNode.frame
|
||||
|
@ -932,7 +932,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
iconNode.isLayerBacked = true
|
||||
iconNode.displaysAsynchronously = false
|
||||
iconNode.displayWithoutProcessing = true
|
||||
strongSelf.containerNode.addSubnode(iconNode)
|
||||
strongSelf.offsetContainerNode.addSubnode(iconNode)
|
||||
strongSelf.credibilityIconNode = iconNode
|
||||
}
|
||||
iconNode.image = currentCredibilityIconImage
|
||||
|
@ -30,6 +30,8 @@ public final class InAppPurchaseManager: NSObject {
|
||||
case cancelled
|
||||
case network
|
||||
case notAllowed
|
||||
case cantMakePayments
|
||||
case assignFailed
|
||||
}
|
||||
|
||||
public enum RestoreState {
|
||||
@ -51,6 +53,7 @@ public final class InAppPurchaseManager: NSObject {
|
||||
case restored(transactionId: String?)
|
||||
case purchasing
|
||||
case failed(error: SKError?)
|
||||
case assignFailed
|
||||
case deferred
|
||||
}
|
||||
|
||||
@ -63,7 +66,7 @@ public final class InAppPurchaseManager: NSObject {
|
||||
|
||||
private let stateQueue = Queue()
|
||||
private var paymentContexts: [String: PaymentTransactionContext] = [:]
|
||||
|
||||
|
||||
private var onRestoreCompletion: ((RestoreState) -> Void)?
|
||||
|
||||
private let disposableSet = DisposableDict<String>()
|
||||
@ -82,6 +85,10 @@ public final class InAppPurchaseManager: NSObject {
|
||||
SKPaymentQueue.default().remove(self)
|
||||
}
|
||||
|
||||
var canMakePayments: Bool {
|
||||
return SKPaymentQueue.canMakePayments()
|
||||
}
|
||||
|
||||
private func requestProducts() {
|
||||
guard !self.premiumProductId.isEmpty else {
|
||||
return
|
||||
@ -119,10 +126,16 @@ public final class InAppPurchaseManager: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
public func buyProduct(_ product: Product, account: Account) -> Signal<PurchaseState, PurchaseError> {
|
||||
Logger.shared.log("InAppPurchaseManager", "Buying product: \(product.skProduct.productIdentifier), price \(product.price)")
|
||||
public func buyProduct(_ product: Product) -> Signal<PurchaseState, PurchaseError> {
|
||||
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)
|
||||
|
||||
let productIdentifier = payment.productIdentifier
|
||||
@ -156,6 +169,8 @@ public final class InAppPurchaseManager: NSObject {
|
||||
} else {
|
||||
subscriber.putError(.generic)
|
||||
}
|
||||
case .assignFailed:
|
||||
subscriber.putError(.assignFailed)
|
||||
case .deferred, .purchasing:
|
||||
break
|
||||
}
|
||||
@ -204,40 +219,35 @@ private func getReceiptData() -> Data? {
|
||||
|
||||
extension InAppPurchaseManager: SKPaymentTransactionObserver {
|
||||
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
|
||||
for transaction in transactions {
|
||||
let productIdentifier = transaction.payment.productIdentifier
|
||||
self.stateQueue.async {
|
||||
self.stateQueue.async {
|
||||
let accountPeerId = "\(self.engine.account.peerId.toInt64())"
|
||||
var transactionsToAssign: [SKPaymentTransaction] = []
|
||||
for transaction in transactions {
|
||||
if let applicationUsername = transaction.payment.applicationUsername, applicationUsername != accountPeerId {
|
||||
continue
|
||||
}
|
||||
|
||||
let productIdentifier = transaction.payment.productIdentifier
|
||||
let transactionState: TransactionState?
|
||||
switch transaction.transactionState {
|
||||
case .purchased:
|
||||
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "none") purchased")
|
||||
let transactionIdentifier = transaction.transactionIdentifier
|
||||
transactionState = .purchased(transactionId: transactionIdentifier)
|
||||
if let transactionIdentifier = transactionIdentifier {
|
||||
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
|
||||
)
|
||||
}
|
||||
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "none") purchased")
|
||||
|
||||
transactionState = .purchased(transactionId: transaction.transactionIdentifier)
|
||||
transactionsToAssign.append(transaction)
|
||||
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
|
||||
transactionState = .restored(transactionId: transactionIdentifier)
|
||||
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)
|
||||
queue.finishTransaction(transaction)
|
||||
case .purchasing:
|
||||
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? "") purchasing")
|
||||
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") purchasing")
|
||||
transactionState = .purchasing
|
||||
case .deferred:
|
||||
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? "") deferred")
|
||||
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") deferred")
|
||||
transactionState = .deferred
|
||||
default:
|
||||
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.engine.payments.sendAppStoreReceipt(receipt: receiptData, restore: true).start(completed: {
|
||||
Logger.shared.log("InAppPurchaseManager", "Sent restored receipt")
|
||||
self.engine.payments.sendAppStoreReceipt(receipt: getReceiptData() ?? Data(), restore: false).start(error: { [weak self] _ in
|
||||
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) {
|
||||
if let onRestoreCompletion = self.onRestoreCompletion {
|
||||
Logger.shared.log("InAppPurchaseManager", "Transactions restoration failed with error \((error as? SKError)?.localizedDescription ?? "")")
|
||||
onRestoreCompletion(.failed)
|
||||
self.onRestoreCompletion = nil
|
||||
Queue.mainQueue().async {
|
||||
if let onRestoreCompletion = self.onRestoreCompletion {
|
||||
Logger.shared.log("InAppPurchaseManager", "Transactions restoration failed with error \((error as? SKError)?.localizedDescription ?? "")")
|
||||
onRestoreCompletion(.failed)
|
||||
self.onRestoreCompletion = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
// }
|
||||
//}
|
@ -191,12 +191,33 @@ private func oldChannelsEntries(presentationData: PresentationData, state: OldCh
|
||||
let count = max(limit, Int32(peers?.count ?? 0))
|
||||
var text: String?
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,44 @@
|
||||
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(
|
||||
name = "PremiumUIResources",
|
||||
@ -17,6 +57,9 @@ swift_library(
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
data = [
|
||||
":PremiumUIBundle",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
|
BIN
submodules/PremiumUI/MetalResources/chars.png
Normal file
BIN
submodules/PremiumUI/MetalResources/chars.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
79
submodules/PremiumUI/MetalResources/matrix.metal
Normal file
79
submodules/PremiumUI/MetalResources/matrix.metal
Normal 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);
|
||||
}
|
BIN
submodules/PremiumUI/MetalResources/random.jpg
Normal file
BIN
submodules/PremiumUI/MetalResources/random.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
Binary file not shown.
BIN
submodules/PremiumUI/Resources/swirl.scn
Normal file
BIN
submodules/PremiumUI/Resources/swirl.scn
Normal file
Binary file not shown.
59
submodules/PremiumUI/Sources/BadgeStarsView.swift
Normal file
59
submodules/PremiumUI/Sources/BadgeStarsView.swift
Normal 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)
|
||||
}
|
||||
}
|
198
submodules/PremiumUI/Sources/DataRainView.swift
Normal file
198
submodules/PremiumUI/Sources/DataRainView.swift
Normal 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()
|
||||
|
||||
}
|
||||
}
|
107
submodules/PremiumUI/Sources/FasterStarsView.swift
Normal file
107
submodules/PremiumUI/Sources/FasterStarsView.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -111,6 +111,7 @@ private final class PhoneView: UIView {
|
||||
self.shimmerMaskView = UIView()
|
||||
self.shimmerBorderView = UIImageView(image: phoneBorderMaskImage)
|
||||
self.shimmerStarView = UIImageView(image: starMaskImage)
|
||||
self.shimmerStarView.alpha = 0.7
|
||||
|
||||
self.backShimmerView = UIView()
|
||||
self.backShimmerView.alpha = 0.0
|
||||
@ -140,10 +141,10 @@ private final class PhoneView: UIView {
|
||||
self.frontShimmerView.mask = self.shimmerMaskView
|
||||
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.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"
|
||||
}
|
||||
|
||||
@ -180,8 +181,16 @@ private final class PhoneView: UIView {
|
||||
|
||||
let status = videoNode.status
|
||||
|> mapToSignal { status -> Signal<MediaPlayerStatus?, NoError> in
|
||||
if let status = status, case .buffering = status.status {
|
||||
return .single(status) |> delay(1.0, queue: Queue.mainQueue())
|
||||
var isLoading = false
|
||||
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 {
|
||||
return .single(status)
|
||||
}
|
||||
@ -205,11 +214,13 @@ private final class PhoneView: UIView {
|
||||
private func updatePlaybackStatus() {
|
||||
var isDisplayingProgress = false
|
||||
if let playbackStatus = self.playbackStatusValue {
|
||||
if case let .buffering(initial, _, progress, _) = playbackStatus.status, initial || !progress.isZero {
|
||||
if case .buffering = playbackStatus.status {
|
||||
isDisplayingProgress = true
|
||||
} else if playbackStatus.status == .playing {
|
||||
isDisplayingProgress = false
|
||||
isDisplayingProgress = playbackStatus.duration.isZero
|
||||
}
|
||||
} else {
|
||||
isDisplayingProgress = true
|
||||
}
|
||||
|
||||
let targetAlpha = isDisplayingProgress ? 1.0 : 0.0
|
||||
@ -280,125 +291,9 @@ private final class PhoneView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
private final class FasterStarsView: 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: "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)
|
||||
}
|
||||
protocol PhoneDemoDecorationView: UIView {
|
||||
func setVisible(_ visible: Bool)
|
||||
func resetAnimation()
|
||||
}
|
||||
|
||||
final class PhoneDemoComponent: Component {
|
||||
@ -411,6 +306,8 @@ final class PhoneDemoComponent: Component {
|
||||
|
||||
enum BackgroundDecoration {
|
||||
case none
|
||||
case dataRain
|
||||
case swirlStars
|
||||
case fasterStars
|
||||
case badgeStars
|
||||
}
|
||||
@ -462,14 +359,12 @@ final class PhoneDemoComponent: Component {
|
||||
private var isCentral = false
|
||||
private var component: PhoneDemoComponent?
|
||||
|
||||
private let starsContainerView: UIView
|
||||
private let decorationContainerView: UIView
|
||||
private var decorationView: PhoneDemoDecorationView?
|
||||
private let containerView: UIView
|
||||
private let phoneView: PhoneView
|
||||
|
||||
private var fasterStarsView: FasterStarsView?
|
||||
private var badgeStarsView: BadgeStarsView?
|
||||
|
||||
private var starsDisposable: Disposable?
|
||||
|
||||
private var playbackStatusDisposable: Disposable?
|
||||
|
||||
public var ready: Signal<Bool, NoError> {
|
||||
if let videoNode = self.phoneView.videoNode {
|
||||
@ -483,8 +378,8 @@ final class PhoneDemoComponent: Component {
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.starsContainerView = UIView(frame: frame)
|
||||
self.starsContainerView.clipsToBounds = true
|
||||
self.decorationContainerView = UIView(frame: frame)
|
||||
self.decorationContainerView.clipsToBounds = true
|
||||
|
||||
self.containerView = UIView(frame: frame)
|
||||
self.containerView.clipsToBounds = true
|
||||
@ -493,7 +388,7 @@ final class PhoneDemoComponent: Component {
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.starsContainerView)
|
||||
self.addSubview(self.decorationContainerView)
|
||||
self.addSubview(self.containerView)
|
||||
self.containerView.addSubview(self.phoneView)
|
||||
}
|
||||
@ -503,41 +398,59 @@ final class PhoneDemoComponent: Component {
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.starsDisposable?.dispose()
|
||||
self.playbackStatusDisposable?.dispose()
|
||||
}
|
||||
|
||||
public func update(component: PhoneDemoComponent, availableSize: CGSize, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
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)
|
||||
|
||||
switch component.decoration {
|
||||
case .none:
|
||||
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:
|
||||
if self.fasterStarsView == nil {
|
||||
let starsView = FasterStarsView(frame: self.starsContainerView.bounds)
|
||||
self.fasterStarsView = starsView
|
||||
self.starsContainerView.addSubview(starsView)
|
||||
if let _ = self.decorationView as? FasterStarsView {
|
||||
} else {
|
||||
let starsView = FasterStarsView(frame: self.decorationContainerView.bounds)
|
||||
self.decorationView = starsView
|
||||
self.decorationContainerView.addSubview(starsView)
|
||||
|
||||
self.starsDisposable = (self.phoneView.playbackStatus
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
if let strongSelf = self, let status = status {
|
||||
self.playbackStatusDisposable = (self.phoneView.playbackStatus
|
||||
|> deliverOnMainQueue).start(next: { [weak starsView] status in
|
||||
if let starsView = starsView, let status = status {
|
||||
if status.timestamp > 8.0 {
|
||||
strongSelf.fasterStarsView?.stopAnimation()
|
||||
starsView.resetAnimation()
|
||||
} else if status.timestamp > 0.85 {
|
||||
strongSelf.fasterStarsView?.startAnimation()
|
||||
starsView.startAnimation()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
case .badgeStars:
|
||||
if self.badgeStarsView == nil {
|
||||
let starsView = BadgeStarsView(frame: self.starsContainerView.bounds)
|
||||
self.badgeStarsView = starsView
|
||||
self.starsContainerView.addSubview(starsView)
|
||||
if let _ = self.decorationView as? BadgeStarsView {
|
||||
} else {
|
||||
let starsView = BadgeStarsView(frame: self.decorationContainerView.bounds)
|
||||
self.decorationView = starsView
|
||||
self.decorationContainerView.addSubview(starsView)
|
||||
}
|
||||
}
|
||||
|
||||
@ -561,8 +474,9 @@ final class PhoneDemoComponent: Component {
|
||||
let isCentral = environment[DemoPageEnvironment.self].isCentral
|
||||
self.isCentral = isCentral
|
||||
|
||||
self.fasterStarsView?.setVisible(isVisible && abs(mappedPosition) < 0.4)
|
||||
self.badgeStarsView?.setVisible(isVisible && abs(mappedPosition) < 0.4)
|
||||
if let decorationView = self.decorationView {
|
||||
decorationView.setVisible(isVisible && abs(mappedPosition) < 0.4)
|
||||
}
|
||||
|
||||
self.phoneView.center = CGPoint(x: availableSize.width / 2.0 + phoneX, y: phoneY)
|
||||
self.phoneView.screenRotation = mappedPosition * -0.7
|
||||
@ -575,7 +489,7 @@ final class PhoneDemoComponent: Component {
|
||||
self.phoneView.play()
|
||||
} else if !isVisible {
|
||||
self.phoneView.reset()
|
||||
self.fasterStarsView?.stopAnimation()
|
||||
self.decorationView?.resetAnimation()
|
||||
}
|
||||
|
||||
if let _ = transition.userData(DemoAnimateInTransition.self), abs(mappedPosition) < .ulpOfOne {
|
||||
|
@ -725,7 +725,8 @@ private final class DemoSheetContent: CombinedComponent {
|
||||
content: AnyComponent(PhoneDemoComponent(
|
||||
context: component.context,
|
||||
position: .bottom,
|
||||
videoFile: configuration.videos["more_upload"]
|
||||
videoFile: configuration.videos["more_upload"],
|
||||
decoration: .dataRain
|
||||
)),
|
||||
title: strings.Premium_UploadSize,
|
||||
text: strings.Premium_UploadSizeInfo,
|
||||
@ -760,7 +761,8 @@ private final class DemoSheetContent: CombinedComponent {
|
||||
content: AnyComponent(PhoneDemoComponent(
|
||||
context: component.context,
|
||||
position: .top,
|
||||
videoFile: configuration.videos["voice_to_text"]
|
||||
videoFile: configuration.videos["voice_to_text"],
|
||||
decoration: .badgeStars
|
||||
)),
|
||||
title: strings.Premium_VoiceToText,
|
||||
text: strings.Premium_VoiceToTextInfo,
|
||||
@ -777,7 +779,8 @@ private final class DemoSheetContent: CombinedComponent {
|
||||
content: AnyComponent(PhoneDemoComponent(
|
||||
context: component.context,
|
||||
position: .bottom,
|
||||
videoFile: configuration.videos["no_ads"]
|
||||
videoFile: configuration.videos["no_ads"],
|
||||
decoration: .swirlStars
|
||||
)),
|
||||
title: strings.Premium_NoAds,
|
||||
text: isStandalone ? strings.Premium_NoAdsStandaloneInfo : strings.Premium_NoAdsInfo,
|
||||
@ -831,7 +834,8 @@ private final class DemoSheetContent: CombinedComponent {
|
||||
content: AnyComponent(PhoneDemoComponent(
|
||||
context: component.context,
|
||||
position: .top,
|
||||
videoFile: configuration.videos["advanced_chat_management"]
|
||||
videoFile: configuration.videos["advanced_chat_management"],
|
||||
decoration: .swirlStars
|
||||
)),
|
||||
title: strings.Premium_ChatManagement,
|
||||
text: strings.Premium_ChatManagementInfo,
|
||||
@ -866,7 +870,8 @@ private final class DemoSheetContent: CombinedComponent {
|
||||
content: AnyComponent(PhoneDemoComponent(
|
||||
context: component.context,
|
||||
position: .top,
|
||||
videoFile: configuration.videos["animated_userpics"]
|
||||
videoFile: configuration.videos["animated_userpics"],
|
||||
decoration: .swirlStars
|
||||
)),
|
||||
title: strings.Premium_Avatar,
|
||||
text: strings.Premium_AvatarInfo,
|
||||
|
@ -1161,13 +1161,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
})
|
||||
|
||||
let termsString: MultilineTextComponent.TextContent
|
||||
if context.component.isPremium == true {
|
||||
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)
|
||||
termsString = .plain(attributedString)
|
||||
} else {
|
||||
termsString = .plain(NSAttributedString())
|
||||
}
|
||||
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)
|
||||
termsString = .plain(attributedString)
|
||||
} else {
|
||||
termsString = .markdown(
|
||||
text: strings.Premium_Terms,
|
||||
@ -1231,6 +1227,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
size.height += 10.0
|
||||
size.height += scrollEnvironment.insets.bottom
|
||||
|
||||
if context.component.source != .settings {
|
||||
size.height += 44.0
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
@ -1393,6 +1393,13 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
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")
|
||||
|
||||
self.inProgress = true
|
||||
@ -1403,7 +1410,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
|> deliverOnMainQueue).start(next: { [weak self] available in
|
||||
if let strongSelf = self {
|
||||
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
|
||||
if let strongSelf = self, case .purchased = status {
|
||||
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
|
||||
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
|
||||
if let strongSelf = self {
|
||||
let _ = updatePremiumPromoConfigurationOnce(account: strongSelf.context.account).start()
|
||||
strongSelf.inProgress = false
|
||||
strongSelf.updateInProgress(false)
|
||||
|
||||
strongSelf.isPremium = true
|
||||
strongSelf.updated(transition: .easeInOut(duration: 0.25))
|
||||
strongSelf.completion()
|
||||
@ -1444,6 +1468,10 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
errorText = presentationData.strings.Premium_Purchase_ErrorNetwork
|
||||
case .notAllowed:
|
||||
errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed
|
||||
case .cantMakePayments:
|
||||
errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments
|
||||
case .assignFailed:
|
||||
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
|
||||
case .cancelled:
|
||||
break
|
||||
}
|
||||
|
@ -819,55 +819,56 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
reachedMaximumLimit = false
|
||||
}
|
||||
|
||||
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 contentSize: CGSize
|
||||
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(
|
||||
component: PremiumLimitDisplayComponent(
|
||||
inactiveColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5),
|
||||
@ -889,58 +890,60 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
context.add(limit
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: limit.size.height / 2.0 + 44.0))
|
||||
)
|
||||
}
|
||||
|
||||
let isIncreaseButton = !reachedMaximumLimit && !isPremiumDisabled
|
||||
let button = button.update(
|
||||
component: SolidRoundedButtonComponent(
|
||||
title: isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK,
|
||||
|
||||
theme: SolidRoundedButtonComponent.Theme(
|
||||
backgroundColor: .black,
|
||||
backgroundColors: gradientColors,
|
||||
foregroundColor: .white
|
||||
let isIncreaseButton = !reachedMaximumLimit && !isPremiumDisabled
|
||||
let button = button.update(
|
||||
component: SolidRoundedButtonComponent(
|
||||
title: isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK,
|
||||
|
||||
theme: SolidRoundedButtonComponent.Theme(
|
||||
backgroundColor: .black,
|
||||
backgroundColors: gradientColors,
|
||||
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,
|
||||
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()
|
||||
}
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
|
||||
transition: context.transition
|
||||
)
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
var textOffset: CGFloat = 228.0
|
||||
if isPremiumDisabled {
|
||||
textOffset -= 68.0
|
||||
}
|
||||
|
||||
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))
|
||||
)
|
||||
|
||||
var textOffset: CGFloat = 228.0
|
||||
if isPremiumDisabled {
|
||||
textOffset -= 68.0
|
||||
contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom)
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -386,13 +386,13 @@ class PremiumStarComponent: Component {
|
||||
animation.fromValue = NSValue(scnMatrix4: initial)
|
||||
animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -1.6, 0.0, 0.0))
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||
animation.beginTime = 0.6
|
||||
animation.beginTime = 1.1
|
||||
animation.duration = 0.9
|
||||
|
||||
let group = CAAnimationGroup()
|
||||
group.animations = [animation]
|
||||
group.beginTime = 1.0
|
||||
group.duration = 3.0
|
||||
group.duration = 4.0
|
||||
group.repeatCount = .infinity
|
||||
|
||||
node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer")
|
||||
|
@ -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 let context: AccountContext
|
||||
|
204
submodules/PremiumUI/Sources/SwirlStarsView.swift
Normal file
204
submodules/PremiumUI/Sources/SwirlStarsView.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -323,46 +323,82 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
private func generatePremiumReactionIcon() -> UIImage? {
|
||||
return generateImage(CGSize(width: 32.0, height: 32.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
if let backgroundImage = UIImage(bundleImageName: "Premium/BackgroundIcon"), let foregroundImage = UIImage(bundleImageName: "Premium/ForegroundIcon") {
|
||||
context.saveGState()
|
||||
if let cgImage = backgroundImage.cgImage {
|
||||
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
|
||||
}
|
||||
|
||||
let colorsArray: [CGColor] = [
|
||||
UIColor(rgb: 0x6B93FF).cgColor,
|
||||
UIColor(rgb: 0x6B93FF).cgColor,
|
||||
UIColor(rgb: 0x976FFF).cgColor,
|
||||
UIColor(rgb: 0xE46ACE).cgColor,
|
||||
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))
|
||||
private let starsCount = 7
|
||||
private final class StarsNode: ASDisplayNode {
|
||||
private let starNodes: [ASImageNode]
|
||||
private var timer: SwiftSignalKit.Timer?
|
||||
|
||||
override init() {
|
||||
let image = UIImage(bundleImageName: "Premium/ReactionsStar")
|
||||
var starNodes: [ASImageNode] = []
|
||||
for _ in 0 ..< starsCount {
|
||||
let node = ASImageNode()
|
||||
node.alpha = 0.0
|
||||
node.image = image
|
||||
node.displaysAsynchronously = false
|
||||
starNodes.append(node)
|
||||
}
|
||||
})
|
||||
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 {
|
||||
var isExtracted: Bool = false
|
||||
|
||||
var backgroundView: UIVisualEffectView?
|
||||
let backgroundMaskNode: ASImageNode
|
||||
let backgroundOverlayNode: ASImageNode
|
||||
let imageNode: ASImageNode
|
||||
let maskImageNode: ASImageNode
|
||||
private var backgroundView: UIVisualEffectView?
|
||||
private let backgroundMaskNode: ASImageNode
|
||||
private let backgroundOverlayNode: ASImageNode
|
||||
private let imageNode: ASImageNode
|
||||
private var starsNode: StarsNode?
|
||||
|
||||
private let maskContainerNode: ASDisplayNode
|
||||
private let maskImageNode: ASImageNode
|
||||
|
||||
init(theme: PresentationTheme) {
|
||||
self.backgroundMaskNode = ASImageNode()
|
||||
@ -372,7 +408,7 @@ final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode {
|
||||
self.backgroundMaskNode.image = UIImage(bundleImageName: "Premium/ReactionsBackground")
|
||||
|
||||
self.backgroundOverlayNode = ASImageNode()
|
||||
self.backgroundOverlayNode.alpha = 0.05
|
||||
self.backgroundOverlayNode.alpha = 0.1
|
||||
self.backgroundOverlayNode.contentMode = .center
|
||||
self.backgroundOverlayNode.displaysAsynchronously = false
|
||||
self.backgroundOverlayNode.isUserInteractionEnabled = false
|
||||
@ -384,20 +420,24 @@ final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode {
|
||||
self.imageNode.isUserInteractionEnabled = false
|
||||
self.imageNode.image = UIImage(bundleImageName: "Premium/ReactionsForeground")
|
||||
|
||||
self.maskContainerNode = ASDisplayNode()
|
||||
|
||||
self.maskImageNode = ASImageNode()
|
||||
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.fill(CGRect(origin: .zero, size: size))
|
||||
|
||||
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.setBlendMode(.clear)
|
||||
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()
|
||||
|
||||
@ -418,10 +458,37 @@ final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode {
|
||||
backgroundView.mask = self.backgroundMaskNode.view
|
||||
self.view.insertSubview(backgroundView, at: 0)
|
||||
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) {
|
||||
|
||||
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) {
|
||||
@ -432,8 +499,7 @@ final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode {
|
||||
self.imageNode.frame = bounds
|
||||
}
|
||||
|
||||
|
||||
var maskNode: ASDisplayNode? {
|
||||
return self.maskImageNode
|
||||
return self.maskContainerNode
|
||||
}
|
||||
}
|
||||
|
@ -118,6 +118,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
|
||||
|
||||
let animationNode = SimpleAnimationNode(animationName: animation, size: CGSize(width: 30.0, height: 30.0))
|
||||
animationNode.customColor = self.theme.foregroundColor
|
||||
animationNode.isUserInteractionEnabled = false
|
||||
self.addSubnode(animationNode)
|
||||
self.animationNode = animationNode
|
||||
|
||||
@ -128,10 +129,10 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
|
||||
if self.gloss {
|
||||
self.animationTimer?.invalidate()
|
||||
|
||||
Queue.mainQueue().after(0.75) {
|
||||
Queue.mainQueue().after(1.25) {
|
||||
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()
|
||||
}, queue: Queue.mainQueue())
|
||||
self.animationTimer = timer
|
||||
@ -227,6 +228,8 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
|
||||
strongSelf.subtitleNode.alpha = 0.55
|
||||
strongSelf.iconNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.iconNode.alpha = 0.55
|
||||
strongSelf.animationNode?.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.animationNode?.alpha = 0.55
|
||||
} else {
|
||||
if strongSelf.buttonBackgroundNode.alpha > 0.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.iconNode.alpha = 1.0
|
||||
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
|
||||
}
|
||||
|
||||
shimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(alpha), gradientSize: 70.0, globalTimeOffset: false, duration: 3.0, horizontal: true)
|
||||
borderShimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(borderAlpha), 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: 4.0, horizontal: true)
|
||||
|
||||
shimmerView.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))
|
||||
animationNode.customColor = self.theme.foregroundColor
|
||||
animationNode.isUserInteractionEnabled = false
|
||||
self.addSubview(animationNode.view)
|
||||
self.animationNode = animationNode
|
||||
|
||||
@ -672,10 +678,10 @@ public final class SolidRoundedButtonView: UIView {
|
||||
if self.gloss {
|
||||
self.animationTimer?.invalidate()
|
||||
|
||||
Queue.mainQueue().after(0.75) {
|
||||
Queue.mainQueue().after(1.25) {
|
||||
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()
|
||||
}, queue: Queue.mainQueue())
|
||||
self.animationTimer = timer
|
||||
@ -784,6 +790,8 @@ public final class SolidRoundedButtonView: UIView {
|
||||
strongSelf.subtitleNode.alpha = 0.55
|
||||
strongSelf.iconNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.iconNode.alpha = 0.55
|
||||
strongSelf.animationNode?.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.animationNode?.alpha = 0.55
|
||||
} else {
|
||||
if strongSelf.buttonBackgroundNode.alpha > 0.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.iconNode.alpha = 1.0
|
||||
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")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.animationTimer?.invalidate()
|
||||
}
|
||||
|
||||
private func setupGloss() {
|
||||
if self.gloss {
|
||||
if self.shimmerView == nil {
|
||||
@ -1002,10 +1016,10 @@ public final class SolidRoundedButtonView: UIView {
|
||||
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)
|
||||
borderShimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(borderAlpha), 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: 4.0, horizontal: true)
|
||||
|
||||
shimmerView.layer.compositingFilter = compositingFilter
|
||||
borderShimmerView.layer.compositingFilter = compositingFilter
|
||||
|
@ -6,6 +6,7 @@ import TelegramApi
|
||||
|
||||
public enum AssignAppStoreTransactionError {
|
||||
case generic
|
||||
case timeout
|
||||
}
|
||||
|
||||
func _internal_sendAppStoreReceipt(account: Account, receipt: Data, restore: Bool) -> Signal<Never, AssignAppStoreTransactionError> {
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
|
||||
|
||||
public enum TogglePeerChatPinnedLocation {
|
||||
case group(PeerGroupId)
|
||||
case filter(Int32)
|
||||
@ -17,6 +16,10 @@ func _internal_toggleItemPinned(postbox: Postbox, accountPeerId: PeerId, locatio
|
||||
return postbox.transaction { transaction -> TogglePeerChatPinnedResult in
|
||||
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 {
|
||||
case let .group(groupId):
|
||||
var itemIds = transaction.getPinnedItemIds(groupId: groupId)
|
||||
@ -39,9 +42,8 @@ func _internal_toggleItemPinned(postbox: Postbox, accountPeerId: PeerId, locatio
|
||||
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
|
||||
if case .root = groupId {
|
||||
@ -75,7 +77,7 @@ func _internal_toggleItemPinned(postbox: Postbox, accountPeerId: PeerId, locatio
|
||||
updatedData.includePeers.removePinnedPeer(peerId)
|
||||
} else {
|
||||
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)
|
||||
|
21
submodules/TelegramUI/Images.xcassets/Premium/ReactionsStar.imageset/Contents.json
vendored
Normal file
21
submodules/TelegramUI/Images.xcassets/Premium/ReactionsStar.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Premium/ReactionsStar.imageset/ministar@3x.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Premium/ReactionsStar.imageset/ministar@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 341 B |
@ -96,7 +96,7 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
|
||||
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 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 {
|
||||
strongSelf.videoNode?.removeFromSupernode()
|
||||
strongSelf.videoContent = videoContent
|
||||
|
@ -3602,7 +3602,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
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.chatTitleView?.pressed = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
|
@ -483,7 +483,7 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode {
|
||||
if let photo = cachedPeerData.photo, let video = photo.videoRepresentations.last, let peerReference = PeerReference(peer) {
|
||||
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 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 {
|
||||
strongSelf.videoNode?.removeFromSupernode()
|
||||
strongSelf.videoContent = videoContent
|
||||
|
@ -16,6 +16,7 @@ import LocalizedPeerData
|
||||
import PhoneNumberFormat
|
||||
import ChatTitleActivityNode
|
||||
import AnimatedCountLabelNode
|
||||
import AccountContext
|
||||
|
||||
private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers])
|
||||
private let subtitleFont = Font.regular(13.0)
|
||||
@ -46,7 +47,7 @@ private enum ChatTitleCredibilityIcon {
|
||||
}
|
||||
|
||||
final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
private let account: Account
|
||||
private let context: AccountContext
|
||||
|
||||
private var theme: PresentationTheme
|
||||
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))]
|
||||
isEnabled = false
|
||||
} 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))]
|
||||
} else {
|
||||
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
|
||||
} else {
|
||||
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))]
|
||||
} else {
|
||||
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))]
|
||||
}
|
||||
}
|
||||
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 {
|
||||
titleCredibilityIcon = .fake
|
||||
} else if peer.isScam {
|
||||
titleCredibilityIcon = .scam
|
||||
} else if peer.isVerified {
|
||||
titleCredibilityIcon = .verified
|
||||
} else if peer.isPremium {
|
||||
} else if peer.isPremium && !premiumConfiguration.isPremiumDisabled {
|
||||
titleCredibilityIcon = .premium
|
||||
}
|
||||
}
|
||||
@ -303,7 +305,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
switch titleContent {
|
||||
case let .peer(peerView, _, isScheduledMessages):
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -405,7 +407,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
case let .peer(peerView, onlineMemberCount, isScheduledMessages):
|
||||
if let peer = peerViewMainPeer(peerView) {
|
||||
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)
|
||||
state = .info(string, .generic)
|
||||
} 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) {
|
||||
self.account = account
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
self.nameDisplayOrder = nameDisplayOrder
|
||||
|
||||
|
||||
self.contentContainer = ASDisplayNode()
|
||||
|
||||
self.titleNode = ImmediateAnimatedCountLabelNode()
|
||||
|
@ -763,6 +763,21 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
||||
}
|
||||
} else if parsedUrl.host == "premium_offer" {
|
||||
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)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2310,7 +2310,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
credibilityIcon = .scam
|
||||
} else if peer.isVerified {
|
||||
credibilityIcon = .verified
|
||||
} else if peer.isPremium && !premiumConfiguration.isPremiumDisabled {
|
||||
} else if peer.isPremium && !premiumConfiguration.isPremiumDisabled && (peer.id != self.context.account.peerId || self.isSettings) {
|
||||
credibilityIcon = .premium
|
||||
} else {
|
||||
credibilityIcon = .none
|
||||
|
@ -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: {
|
||||
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: {
|
||||
interaction.openSettings(.logout)
|
||||
@ -6275,13 +6291,19 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
case .username:
|
||||
push(usernameSetupController(context: self.context))
|
||||
case .addAccount:
|
||||
var maximumAvailableAccounts: Int = 3
|
||||
if self.data?.peer?.isPremium == true && !self.context.account.testingEnvironment {
|
||||
maximumAvailableAccounts = 4
|
||||
}
|
||||
var count: Int = 1
|
||||
if let settings = self.data?.globalSettings {
|
||||
for (accountContext, peer, _) in settings.accountsAndPeers {
|
||||
let _ = (activeAccountsAndPeers(context: context)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
).start(next: { [weak self] accountAndPeer, accountsAndPeers in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
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 peer.isPremium {
|
||||
maximumAvailableAccounts = 4
|
||||
@ -6289,23 +6311,23 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if count >= maximumAvailableAccounts {
|
||||
let context = self.context
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = PremiumLimitScreen(context: context, subject: .accounts, count: Int32(count), action: {
|
||||
let controller = PremiumIntroScreen(context: context, source: .accounts)
|
||||
replaceImpl?(controller)
|
||||
})
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
|
||||
if count >= maximumAvailableAccounts {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = PremiumLimitScreen(context: strongSelf.context, subject: .accounts, count: Int32(count), action: {
|
||||
let controller = PremiumIntroScreen(context: strongSelf.context, source: .accounts)
|
||||
replaceImpl?(controller)
|
||||
})
|
||||
replaceImpl = { [weak controller] c in
|
||||
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:
|
||||
if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone {
|
||||
if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController {
|
||||
|
Loading…
x
Reference in New Issue
Block a user