diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 0f820d369f..cc0aa1e301 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7711,5 +7711,6 @@ 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."; "Settings.Premium" = "Telegram Premium"; diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 2c2f9345b2..7aff6992c1 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -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 diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index 08bda86473..b1df3d7b7a 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -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() @@ -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 { - Logger.shared.log("InAppPurchaseManager", "Buying product: \(product.skProduct.productIdentifier), price \(product.price)") + public func buyProduct(_ product: Product) -> Signal { + 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 } @@ -205,39 +220,48 @@ private func getReceiptData() -> Data? { extension InAppPurchaseManager: SKPaymentTransactionObserver { public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for transaction in transactions { + let accountPeerId = "\(self.engine.account.peerId.toInt64())" + if let applicationUsername = transaction.payment.applicationUsername, applicationUsername != accountPeerId { + continue + } + let productIdentifier = transaction.payment.productIdentifier self.stateQueue.async { let transactionState: TransactionState? switch transaction.transactionState { case .purchased: - Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "none") purchased") + Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "none") purchased") let transactionIdentifier = transaction.transactionIdentifier transactionState = .purchased(transactionId: transactionIdentifier) 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") + self.engine.payments.sendAppStoreReceipt(receipt: getReceiptData() ?? Data(), restore: false).start(error: { [weak self] _ in + Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") failed to assign AppStore transaction") queue.finishTransaction(transaction) + + if let strongSelf = self, let context = strongSelf.paymentContexts[productIdentifier] { + context.subscriber(.assignFailed) + } }, completed: { - Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? "") successfully assigned AppStore transaction") + Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") successfully assigned AppStore transaction") queue.finishTransaction(transaction) }), forKey: transactionIdentifier ) } 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 diff --git a/submodules/InAppPurchaseManager/Sources/StoredTransactionState.swift b/submodules/InAppPurchaseManager/Sources/StoredTransactionState.swift new file mode 100644 index 0000000000..3f0f328d81 --- /dev/null +++ b/submodules/InAppPurchaseManager/Sources/StoredTransactionState.swift @@ -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 { +// 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 { +// 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) +// } +//} diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 7cf7d94251..12c178b056 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -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 = + """ + CFBundleIdentifier + org.telegram.PremiumUI + CFBundleDevelopmentRegion + en + CFBundleName + PremiumUI + """ +) + +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", diff --git a/submodules/PremiumUI/MetalResources/chars.png b/submodules/PremiumUI/MetalResources/chars.png new file mode 100644 index 0000000000..1a00dd5f4e Binary files /dev/null and b/submodules/PremiumUI/MetalResources/chars.png differ diff --git a/submodules/PremiumUI/MetalResources/matrix.metal b/submodules/PremiumUI/MetalResources/matrix.metal new file mode 100644 index 0000000000..9cbfd8e745 --- /dev/null +++ b/submodules/PremiumUI/MetalResources/matrix.metal @@ -0,0 +1,79 @@ +#include + +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 symbolTexture, + texture2d 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 + 0.1; + 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 symbolTexture [[ texture(0) ]], + texture2d 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); +} diff --git a/submodules/PremiumUI/MetalResources/random.jpg b/submodules/PremiumUI/MetalResources/random.jpg new file mode 100644 index 0000000000..708398ba99 Binary files /dev/null and b/submodules/PremiumUI/MetalResources/random.jpg differ diff --git a/submodules/PremiumUI/Resources/badge.scn b/submodules/PremiumUI/Resources/badge.scn index 48992cda49..40aa6f0b0d 100644 Binary files a/submodules/PremiumUI/Resources/badge.scn and b/submodules/PremiumUI/Resources/badge.scn differ diff --git a/submodules/PremiumUI/Resources/lightspeed.scn b/submodules/PremiumUI/Resources/lightspeed.scn index e7ccc59abd..660d751f6d 100644 Binary files a/submodules/PremiumUI/Resources/lightspeed.scn and b/submodules/PremiumUI/Resources/lightspeed.scn differ diff --git a/submodules/PremiumUI/Resources/swirl.scn b/submodules/PremiumUI/Resources/swirl.scn new file mode 100644 index 0000000000..f5f50794de Binary files /dev/null and b/submodules/PremiumUI/Resources/swirl.scn differ diff --git a/submodules/PremiumUI/Sources/BadgeStarsView.swift b/submodules/PremiumUI/Sources/BadgeStarsView.swift new file mode 100644 index 0000000000..78c59637bd --- /dev/null +++ b/submodules/PremiumUI/Sources/BadgeStarsView.swift @@ -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) + } +} diff --git a/submodules/PremiumUI/Sources/DataRainView.swift b/submodules/PremiumUI/Sources/DataRainView.swift new file mode 100644 index 0000000000..122971f7d7 --- /dev/null +++ b/submodules/PremiumUI/Sources/DataRainView.swift @@ -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.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() + + } +} diff --git a/submodules/PremiumUI/Sources/FasterStarsView.swift b/submodules/PremiumUI/Sources/FasterStarsView.swift new file mode 100644 index 0000000000..f80b621f70 --- /dev/null +++ b/submodules/PremiumUI/Sources/FasterStarsView.swift @@ -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) + } +} diff --git a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift index 9a6515eb8c..e1fb0878cd 100644 --- a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift +++ b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift @@ -280,125 +280,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 +295,8 @@ final class PhoneDemoComponent: Component { enum BackgroundDecoration { case none + case dataRain + case swirlStars case fasterStars case badgeStars } @@ -462,14 +348,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 { if let videoNode = self.phoneView.videoNode { @@ -483,8 +367,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 +377,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) } @@ -502,42 +386,60 @@ final class PhoneDemoComponent: Component { fatalError("init(coder:) has not been implemented") } - deinit { - self.starsDisposable?.dispose() - } +// deinit { +// self.playbackStatusDisposable?.dispose() +// } public func update(component: PhoneDemoComponent, availableSize: CGSize, environment: Environment, 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 +463,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 +478,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 { diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 7c2a77decd..327de73c3b 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -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, diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 313505e395..cae6c1b59d 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -1161,13 +1161,10 @@ 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 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 = .markdown( text: strings.Premium_Terms, @@ -1403,7 +1400,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,8 +1415,14 @@ private final class PremiumIntroScreenComponent: CombinedComponent { |> mapToSignal { _ -> Signal in return .never() } + |> timeout(15.0, queue: Queue.mainQueue(), alternate: .fail(.timeout)) |> deliverOnMainQueue).start(error: { _ in - + 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() @@ -1444,6 +1447,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 } diff --git a/submodules/PremiumUI/Sources/SwirlStarsView.swift b/submodules/PremiumUI/Sources/SwirlStarsView.swift new file mode 100644 index 0000000000..6b80754777 --- /dev/null +++ b/submodules/PremiumUI/Sources/SwirlStarsView.swift @@ -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) + } +} diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 62ef7d5b8b..00956b337e 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -321,46 +321,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() @@ -370,7 +406,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 @@ -382,20 +418,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() @@ -416,10 +456,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) { @@ -430,8 +497,7 @@ final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode { self.imageNode.frame = bounds } - var maskNode: ASDisplayNode? { - return self.maskImageNode + return self.maskContainerNode } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift index 8dd0f98cef..4a8b1c68b0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift @@ -6,6 +6,7 @@ import TelegramApi public enum AssignAppStoreTransactionError { case generic + case timeout } func _internal_sendAppStoreReceipt(account: Account, receipt: Data, restore: Bool) -> Signal { diff --git a/submodules/TelegramUI/Images.xcassets/Premium/ReactionsStar.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/ReactionsStar.imageset/Contents.json new file mode 100644 index 0000000000..18355ee5bf --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/ReactionsStar.imageset/Contents.json @@ -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 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/ReactionsStar.imageset/ministar@3x.png b/submodules/TelegramUI/Images.xcassets/Premium/ReactionsStar.imageset/ministar@3x.png new file mode 100644 index 0000000000..efd6a2ef5a Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/ReactionsStar.imageset/ministar@3x.png differ