From e25f926342c722c6970c1221ffa6fd3c2965ec31 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 17 Nov 2023 21:48:46 +0400 Subject: [PATCH] [WIP] Colored profile banners --- .../ContainedViewLayoutTransition.swift | 16 +- .../Display/Source/DisplayLinkAnimator.swift | 153 +- submodules/Display/Source/ListView.swift | 2 +- .../DrawingUI/Sources/DrawingTextEntity.swift | 2 +- .../Sources/ManagedAnimationNode.swift | 4 +- .../Sources/MediaPlayerScrubbingNode.swift | 2 +- .../PremiumUI/Sources/DataRainView.swift | 2 +- .../Sources/PrivateCallScreen.swift | 4 +- .../Sources/ShutterBlobView.swift | 2 +- .../DustEffect/Metal/DustEffectShaders.metal | 18 + .../DustEffect/Sources/DustEffectLayer.swift | 34 +- .../Sources/EmojiSearchStatusComponent.swift | 4 +- .../Sources/LottieComponent.swift | 8 +- .../Sources/VideoScrubberComponent.swift | 2 +- .../PeerInfo/PeerInfoCoverComponent/BUILD | 29 + .../Sources/PeerInfoCoverComponent.swift | 377 +++ .../Components/PeerInfo/PeerInfoScreen/BUILD | 1 + .../Sources/DynamicIslandBlurNode.swift | 133 + .../Sources/PeerInfoAvatarListNode.swift | 177 ++ ...PeerInfoAvatarTransformContainerNode.swift | 415 +++ .../Sources/PeerInfoEditingAvatarNode.swift | 233 ++ .../PeerInfoEditingAvatarOverlayNode.swift | 149 + .../PeerInfoHeaderActionButtonNode.swift | 88 + .../Sources/PeerInfoHeaderButtonNode.swift | 264 ++ .../PeerInfoHeaderEditingContentNode.swift | 183 ++ ...PeerInfoHeaderMultiLineTextFieldNode.swift | 226 ++ .../PeerInfoHeaderNavigationButton.swift | 286 ++ ...oHeaderNavigationButtonContainerNode.swift | 249 ++ .../Sources/PeerInfoHeaderNode.swift | 2629 +---------------- ...eerInfoHeaderSingleLineTextFieldNode.swift | 151 + .../Sources/PeerInfoScreen.swift | 17 +- .../Sources/PieChartComponent.swift | 18 +- .../TelegramUI/Sources/ChatController.swift | 25 + .../Sources/ChatHistoryListNode.swift | 80 +- submodules/Utils/LokiRng/BUILD | 24 + .../LokiRng/PublicHeaders/LokiRng/LokiRng.h | 14 + submodules/Utils/LokiRng/Sources/LokiRng.mm | 64 + .../Public/Animation/AnimationView.swift | 2 +- 38 files changed, 3431 insertions(+), 2656 deletions(-) create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/BUILD create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarListNode.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarOverlayNode.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderActionButtonNode.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderButtonNode.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderMultiLineTextFieldNode.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderSingleLineTextFieldNode.swift create mode 100644 submodules/Utils/LokiRng/BUILD create mode 100644 submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h create mode 100644 submodules/Utils/LokiRng/Sources/LokiRng.mm diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 36fe8dc3c6..cbdc55b4a2 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -1250,7 +1250,11 @@ public extension ContainedViewLayoutTransition { completion?(true) return } - let t = node.layer.sublayerTransform + self.updateSublayerTransformScaleAdditive(layer: node.layer, scale: scale, completion: completion) + } + + func updateSublayerTransformScaleAdditive(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { + let t = layer.sublayerTransform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) if currentScale.isEqual(to: scale) { if let completion = completion { @@ -1261,16 +1265,16 @@ public extension ContainedViewLayoutTransition { switch self { case .immediate: - node.layer.removeAnimation(forKey: "sublayerTransform") - node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) + layer.removeAnimation(forKey: "sublayerTransform") + layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) if let completion = completion { completion(true) } case let .animated(duration, curve): - let t = node.layer.sublayerTransform + let t = layer.sublayerTransform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) - node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) - node.layer.animate(from: -(scale - currentScale) as NSNumber, to: 0.0 as NSNumber, keyPath: "sublayerTransform.scale", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: true, completion: { + layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) + layer.animate(from: -(scale - currentScale) as NSNumber, to: 0.0 as NSNumber, keyPath: "sublayerTransform.scale", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: true, completion: { result in if let completion = completion { completion(result) diff --git a/submodules/Display/Source/DisplayLinkAnimator.swift b/submodules/Display/Source/DisplayLinkAnimator.swift index 2762374341..d14c205326 100644 --- a/submodules/Display/Source/DisplayLinkAnimator.swift +++ b/submodules/Display/Source/DisplayLinkAnimator.swift @@ -8,28 +8,45 @@ public protocol SharedDisplayLinkDriverLink: AnyObject { } public final class SharedDisplayLinkDriver { + public enum FramesPerSecond: Comparable { + case fps(Int) + case max + + public static func <(lhs: FramesPerSecond, rhs: FramesPerSecond) -> Bool { + switch lhs { + case let .fps(lhsFps): + switch rhs { + case let .fps(rhsFps): + return lhsFps < rhsFps + case .max: + return true + } + case .max: + return false + } + } + } + public typealias Link = SharedDisplayLinkDriverLink public static let shared = SharedDisplayLinkDriver() - private let useNative: Bool - public final class LinkImpl: Link { private let driver: SharedDisplayLinkDriver - public let needsHighestFramerate: Bool - let update: () -> Void + public let framesPerSecond: FramesPerSecond + let update: (CGFloat) -> Void var isValid: Bool = true public var isPaused: Bool = false { didSet { if self.isPaused != oldValue { - driver.requestUpdate() + self.driver.requestUpdate() } } } - init(driver: SharedDisplayLinkDriver, needsHighestFramerate: Bool, update: @escaping () -> Void) { + init(driver: SharedDisplayLinkDriver, framesPerSecond: FramesPerSecond, update: @escaping (CGFloat) -> Void) { self.driver = driver - self.needsHighestFramerate = needsHighestFramerate + self.framesPerSecond = framesPerSecond self.update = update } @@ -38,65 +55,24 @@ public final class SharedDisplayLinkDriver { } } - public final class NativeLinkImpl: Link { - private var displayLink: CADisplayLink? - - public var isPaused: Bool = false { - didSet { - self.displayLink?.isPaused = self.isPaused - } - } - - init(needsHighestFramerate: Bool, update: @escaping () -> Void) { - let displayLink = CADisplayLink(target: DisplayLinkTarget { - update() - }, selector: #selector(DisplayLinkTarget.event)) - - if #available(iOS 15.0, *) { - let maxFps = Float(UIScreen.main.maximumFramesPerSecond) - if maxFps > 61.0 { - let frameRateRange: CAFrameRateRange - if needsHighestFramerate { - frameRateRange = CAFrameRateRange(minimum: 30.0, maximum: 120.0, preferred: 120.0) - } else { - frameRateRange = .default - } - if displayLink.preferredFrameRateRange != frameRateRange { - displayLink.preferredFrameRateRange = frameRateRange - } - } - } - - self.displayLink = displayLink - displayLink.add(to: .main, forMode: .common) - } - - deinit { - self.displayLink?.invalidate() - } - - public func invalidate() { - self.displayLink?.invalidate() - } - } - private final class RequestContext { weak var link: LinkImpl? + let framesPerSecond: FramesPerSecond - init(link: LinkImpl) { + var lastDuration: Double = 0.0 + + init(link: LinkImpl, framesPerSecond: FramesPerSecond) { self.link = link + self.framesPerSecond = framesPerSecond } } private var displayLink: CADisplayLink? - private var hasRequestedHighestFramerate: Bool = false private var requests: [RequestContext] = [] private var isInForeground: Bool = false private init() { - self.useNative = false - let _ = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil, using: { [weak self] _ in guard let self else { return @@ -139,10 +115,12 @@ public final class SharedDisplayLinkDriver { private func update() { var hasActiveItems = false - var needHighestFramerate = false + var maxFramesPerSecond: FramesPerSecond = .fps(30) for request in self.requests { if let link = request.link { - needHighestFramerate = link.needsHighestFramerate + if link.framesPerSecond > maxFramesPerSecond { + maxFramesPerSecond = link.framesPerSecond + } if link.isValid && !link.isPaused { hasActiveItems = true break @@ -163,10 +141,15 @@ public final class SharedDisplayLinkDriver { let maxFps = Float(UIScreen.main.maximumFramesPerSecond) if maxFps > 61.0 { let frameRateRange: CAFrameRateRange - if needHighestFramerate { + switch maxFramesPerSecond { + case let .fps(fps): + if fps > 60 { + frameRateRange = CAFrameRateRange(minimum: 30.0, maximum: 120.0, preferred: 120.0) + } else { + frameRateRange = .default + } + case .max: frameRateRange = CAFrameRateRange(minimum: 30.0, maximum: 120.0, preferred: 120.0) - } else { - frameRateRange = .default } if displayLink.preferredFrameRateRange != frameRateRange { displayLink.preferredFrameRateRange = frameRateRange @@ -182,12 +165,28 @@ public final class SharedDisplayLinkDriver { } } - @objc private func displayLinkEvent() { + @objc private func displayLinkEvent(displayLink: CADisplayLink) { + let duration = displayLink.duration + var removeIndices: [Int]? - for i in 0 ..< self.requests.count { - if let link = self.requests[i].link, link.isValid { + loop: for i in 0 ..< self.requests.count { + let request = self.requests[i] + if let link = request.link, link.isValid { if !link.isPaused { - link.update() + switch request.framesPerSecond { + case let .fps(value): + let secondsPerFrame = 1.0 / CGFloat(value) + request.lastDuration += duration + if request.lastDuration >= secondsPerFrame * 0.99 { + } else { + continue loop + } + case .max: + break + } + + request.lastDuration = 0.0 + link.update(duration) } } else { if removeIndices == nil { @@ -208,29 +207,25 @@ public final class SharedDisplayLinkDriver { } } - public func add(needsHighestFramerate: Bool = true, _ update: @escaping () -> Void) -> Link { - if self.useNative { - return NativeLinkImpl(needsHighestFramerate: needsHighestFramerate, update: update) - } else { - let link = LinkImpl(driver: self, needsHighestFramerate: needsHighestFramerate, update: update) - self.requests.append(RequestContext(link: link)) - - self.update() - - return link - } + public func add(framesPerSecond: FramesPerSecond = .fps(60), _ update: @escaping (CGFloat) -> Void) -> Link { + let link = LinkImpl(driver: self, framesPerSecond: framesPerSecond, update: update) + self.requests.append(RequestContext(link: link, framesPerSecond: framesPerSecond)) + + self.update() + + return link } } public final class DisplayLinkTarget: NSObject { - private let f: () -> Void + private let f: (CADisplayLink) -> Void - public init(_ f: @escaping () -> Void) { + public init(_ f: @escaping (CADisplayLink) -> Void) { self.f = f } - @objc public func event() { - self.f() + @objc public func event(_ displayLink: CADisplayLink) { + self.f(displayLink) } } @@ -253,7 +248,7 @@ public final class DisplayLinkAnimator { self.startTime = CACurrentMediaTime() - self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in + self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in self?.tick() } self.displayLink?.isPaused = false @@ -308,7 +303,7 @@ public final class ConstantDisplayLinkAnimator { didSet { if self.isPaused != oldValue { if !self.isPaused && self.displayLink == nil { - let displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in + let displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in self?.tick() } self.displayLink = displayLink diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 3e241ce420..2aea72cef9 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -394,7 +394,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture didSet { if self.isAuxiliaryDisplayLinkEnabled { if self.auxiliaryDisplayLinkHandle == nil { - self.auxiliaryDisplayLinkHandle = SharedDisplayLinkDriver.shared.add(needsHighestFramerate: true, { [weak self] in + self.auxiliaryDisplayLinkHandle = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in guard let self else { return } diff --git a/submodules/DrawingUI/Sources/DrawingTextEntity.swift b/submodules/DrawingUI/Sources/DrawingTextEntity.swift index 0388e89aeb..7edc82a173 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntity.swift @@ -535,7 +535,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate let displayLinkStart = CACurrentMediaTime() self.displayLinkStart = displayLinkStart - self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in + self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in if let strongSelf = self { let currentTime = CACurrentMediaTime() if let previousDisplayLinkTime = strongSelf.previousDisplayLinkTime, currentTime < previousDisplayLinkTime + delta { diff --git a/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift b/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift index 2343704a2b..5c834aa457 100644 --- a/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift +++ b/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift @@ -144,7 +144,7 @@ public struct ManagedAnimationItem { open class ManagedAnimationNode: ASDisplayNode { public let intrinsicSize: CGSize - private let imageNode: ASImageNode + public let imageNode: ASImageNode private let displayLink: SharedDisplayLinkDriver.Link public var imageUpdated: ((UIImage) -> Void)? @@ -182,7 +182,7 @@ open class ManagedAnimationNode: ASDisplayNode { self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize) var displayLinkUpdate: (() -> Void)? - self.displayLink = SharedDisplayLinkDriver.shared.add { + self.displayLink = SharedDisplayLinkDriver.shared.add { _ in displayLinkUpdate?() } diff --git a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift index ea29ba5ef2..025c093993 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift @@ -813,7 +813,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { if needsAnimation { if self.displayLink == nil { - let displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in + let displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in self?.updateProgress() } self.displayLink = displayLink diff --git a/submodules/PremiumUI/Sources/DataRainView.swift b/submodules/PremiumUI/Sources/DataRainView.swift index a6ac8ed259..3aad07bb0d 100644 --- a/submodules/PremiumUI/Sources/DataRainView.swift +++ b/submodules/PremiumUI/Sources/DataRainView.swift @@ -87,7 +87,7 @@ public final class MatrixView: MTKView, MTKViewDelegate, PhoneDemoDecorationView self.framebufferOnly = true - self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in + self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in self?.tick() } self.displayLink?.isPaused = true diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index a1d7b0dce6..adfc27257f 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -188,12 +188,12 @@ public final class PrivateCallScreen: OverlayMaskContainerView { guard let self else { return } - self.audioLevelUpdateSubscription = SharedDisplayLinkDriver.shared.add(needsHighestFramerate: false, { [weak self] in + self.audioLevelUpdateSubscription = SharedDisplayLinkDriver.shared.add { [weak self] _ in guard let self else { return } self.attenuateAudioLevelStep() - }) + } } (self.layer as? SimpleLayer)?.didExitHierarchy = { [weak self] in guard let self else { diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift index 1d5e0b34d6..f0c69d9a5e 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift @@ -279,7 +279,7 @@ final class ShutterBlobView: UIView { self.isOpaque = false self.backgroundColor = .clear - self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in + self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in self?.tick() } self.displayLink?.isPaused = true diff --git a/submodules/TelegramUI/Components/DustEffect/Metal/DustEffectShaders.metal b/submodules/TelegramUI/Components/DustEffect/Metal/DustEffectShaders.metal index 64a439a3d7..152adb1c90 100644 --- a/submodules/TelegramUI/Components/DustEffect/Metal/DustEffectShaders.metal +++ b/submodules/TelegramUI/Components/DustEffect/Metal/DustEffectShaders.metal @@ -38,6 +38,24 @@ struct Particle { float lifetime; }; +kernel void dustEffectInitializeParticle( + device Particle *particles [[ buffer(0) ]], + uint gid [[ thread_position_in_grid ]] +) { + Loki rng = Loki(gid); + + Particle particle; + particle.offsetFromBasePosition = packed_float2(0.0, 0.0); + + float direction = rng.rand() * (3.14159265 * 2.0); + float velocity = (0.1 + rng.rand() * (0.2 - 0.1)) * 420.0; + particle.velocity = packed_float2(cos(direction) * velocity, sin(direction) * velocity); + + particle.lifetime = 0.7 + rng.rand() * (1.5 - 0.7); + + particles[gid] = particle; +} + float particleEaseInWindowFunction(float t) { return t; } diff --git a/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift b/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift index 9b6aa7b63a..6947eced45 100644 --- a/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift +++ b/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift @@ -36,6 +36,7 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject let texture: MTLTexture var phase: Float = 0 + var particleBufferIsInitialized: Bool = false var particleBuffer: SharedBuffer? init?(frame: CGRect, image: UIImage) { @@ -78,18 +79,29 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject } final class DustComputeState: ComputeState { + let computePipelineStateInitializeParticle: MTLComputePipelineState let computePipelineStateUpdateParticle: MTLComputePipelineState required init?(device: MTLDevice) { guard let library = metalLibrary(device: device) else { return nil } + + guard let functionDustEffectInitializeParticle = library.makeFunction(name: "dustEffectInitializeParticle") else { + return nil + } + guard let computePipelineStateInitializeParticle = try? device.makeComputePipelineState(function: functionDustEffectInitializeParticle) else { + return nil + } + self.computePipelineStateInitializeParticle = computePipelineStateInitializeParticle + guard let functionDustEffectUpdateParticle = library.makeFunction(name: "dustEffectUpdateParticle") else { return nil } guard let computePipelineStateUpdateParticle = try? device.makeComputePipelineState(function: functionDustEffectUpdateParticle) else { return nil } + self.computePipelineStateUpdateParticle = computePipelineStateUpdateParticle } } @@ -127,7 +139,7 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject fatalError("init(coder:) has not been implemented") } - private func updateItems() { + private func updateItems(deltaTime: Double) { var didRemoveItems = false for i in (0 ..< self.items.count).reversed() { self.items[i].phase += (1.0 / 60.0) / Float(UIView.animationDurationFactor()) @@ -147,13 +159,13 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject private func updateNeedsAnimation() { if !self.items.isEmpty && self.isInHierarchy { if self.updateLink == nil { - self.updateLink = SharedDisplayLinkDriver.shared.add { [weak self] in + self.updateLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .fps(60), { [weak self] deltaTime in guard let self else { return } - self.updateItems() + self.updateItems(deltaTime: deltaTime) self.setNeedsUpdate() - } + }) } } else { if self.updateLink != nil { @@ -189,7 +201,7 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject if let particleBuffer = MetalEngine.shared.sharedBuffer(spec: BufferSpec(length: particleCount * 4 * (4 + 1))) { item.particleBuffer = particleBuffer - let particles = particleBuffer.buffer.contents().assumingMemoryBound(to: Float.self) + /*let particles = particleBuffer.buffer.contents().assumingMemoryBound(to: Float.self) for i in 0 ..< particleCount { particles[i * 5 + 0] = 0.0; particles[i * 5 + 1] = 0.0; @@ -200,7 +212,7 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject particles[i * 5 + 3] = sin(direction) * velocity particles[i * 5 + 4] = Float.random(in: 0.7 ... 1.5) - } + }*/ } } } @@ -217,6 +229,7 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject guard let particleBuffer = item.particleBuffer else { continue } + let itemFrame = item.frame let particleColumnCount = Int(itemFrame.width) let particleRowCount = Int(itemFrame.height) @@ -224,8 +237,15 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject let threadgroupSize = MTLSize(width: 32, height: 1, depth: 1) let threadgroupCount = MTLSize(width: (particleRowCount * particleColumnCount + threadgroupSize.width - 1) / threadgroupSize.width, height: 1, depth: 1) - computeEncoder.setComputePipelineState(state.computePipelineStateUpdateParticle) computeEncoder.setBuffer(particleBuffer.buffer, offset: 0, index: 0) + + if !item.particleBufferIsInitialized { + item.particleBufferIsInitialized = true + computeEncoder.setComputePipelineState(state.computePipelineStateInitializeParticle) + computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) + } + + computeEncoder.setComputePipelineState(state.computePipelineStateUpdateParticle) var particleCount = SIMD2(UInt32(particleColumnCount), UInt32(particleRowCount)) computeEncoder.setBytes(&particleCount, length: 4 * 2, index: 1) var phase = item.phase diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift index 8d35cb2bf3..4b498d229b 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift @@ -771,12 +771,12 @@ final class EmojiSearchStatusComponent: Component { if needsAnimation { if self.displayLink == nil { var counter = 0 - self.displayLink = SharedDisplayLinkDriver.shared.add(needsHighestFramerate: false, { [weak self] in + self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in counter += 1 if counter % 1 == 0 { self?.updateAnimation() } - }) + } } } else { if let displayLink = self.displayLink { diff --git a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift index b7111f6d38..b1c016e632 100644 --- a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift +++ b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift @@ -263,23 +263,23 @@ public final class LottieComponent: Component { self.currentFrameStartTime = CACurrentMediaTime() if self.displayLink == nil { - self.displayLink = SharedDisplayLinkDriver.shared.add(needsHighestFramerate: false, { [weak self] in + self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in guard let self else { return } self.advanceIfNeeded() - }) + } } }) } else { self.currentFrameStartTime = CACurrentMediaTime() if self.displayLink == nil { - self.displayLink = SharedDisplayLinkDriver.shared.add(needsHighestFramerate: false, { [weak self] in + self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in guard let self else { return } self.advanceIfNeeded() - }) + } } } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift index 7bdc8a6b11..da8cd708da 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift @@ -281,7 +281,7 @@ final class VideoScrubberComponent: Component { self.cursorView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePositionHandlePan(_:)))) - self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in + self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in self?.updateCursorPosition() } self.displayLink?.isPaused = true diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/BUILD new file mode 100644 index 0000000000..36d9e699d3 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/BUILD @@ -0,0 +1,29 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PeerInfoCoverComponent", + module_name = "PeerInfoCoverComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + "//submodules/Utils/LokiRng", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift new file mode 100644 index 0000000000..e02b42c0e9 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift @@ -0,0 +1,377 @@ +import Foundation +import AsyncDisplayKit +import Display +import ComponentFlow +import ComponentDisplayAdapters +import AnimationCache +import MultiAnimationRenderer +import TelegramCore +import AccountContext +import SwiftSignalKit +import EmojiTextAttachmentView +import LokiRng + +private final class PatternContentsTarget: MultiAnimationRenderTarget { + private let imageUpdated: () -> Void + + init(imageUpdated: @escaping () -> Void) { + self.imageUpdated = imageUpdated + + super.init() + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + override func transitionToContents(_ contents: AnyObject, didLoop: Bool) { + self.contents = contents + self.imageUpdated() + } +} + +private func windowFunction(t: CGFloat) -> CGFloat { + return bezierPoint(0.6, 0.0, 0.4, 1.0, t) +} + +private func patternScaleValueAt(fraction: CGFloat, t: CGFloat, reverse: Bool) -> CGFloat { + let windowSize: CGFloat = 0.8 + + let effectiveT: CGFloat + let windowStartOffset: CGFloat + let windowEndOffset: CGFloat + if reverse { + effectiveT = 1.0 - t + windowStartOffset = 1.0 + windowEndOffset = -windowSize + } else { + effectiveT = t + windowStartOffset = -windowSize + windowEndOffset = 1.0 + } + + let windowPosition = (1.0 - fraction) * windowStartOffset + fraction * windowEndOffset + let windowT = max(0.0, min(windowSize, effectiveT - windowPosition)) / windowSize + let localT = 1.0 - windowFunction(t: windowT) + + return localT +} + +public final class PeerInfoCoverComponent: Component { + public let context: AccountContext + public let peer: EnginePeer? + public let avatarCenter: CGPoint + public let avatarScale: CGFloat + public let avatarTransitionFraction: CGFloat + public let patternTransitionFraction: CGFloat + + public init( + context: AccountContext, + peer: EnginePeer?, + avatarCenter: CGPoint, + avatarScale: CGFloat, + avatarTransitionFraction: CGFloat, + patternTransitionFraction: CGFloat + ) { + self.context = context + self.peer = peer + self.avatarCenter = avatarCenter + self.avatarScale = avatarScale + self.avatarTransitionFraction = avatarTransitionFraction + self.patternTransitionFraction = patternTransitionFraction + } + + public static func ==(lhs: PeerInfoCoverComponent, rhs: PeerInfoCoverComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.avatarCenter != rhs.avatarCenter { + return false + } + if lhs.avatarScale != rhs.avatarScale { + return false + } + if lhs.avatarTransitionFraction != rhs.avatarTransitionFraction { + return false + } + if lhs.patternTransitionFraction != rhs.patternTransitionFraction { + return false + } + return true + } + + public final class View: UIView { + private let backgroundView: UIView + private let avatarBackgroundPatternContainer: UIView + private let avatarBackgroundGradientLayer: SimpleGradientLayer + private let avatarBackgroundPatternView: UIView + private let backgroundPatternContainer: UIView + + private var component: PeerInfoCoverComponent? + private var state: EmptyComponentState? + + private var patternContentsTarget: PatternContentsTarget? + private var avatarPatternContentLayers: [SimpleLayer] = [] + private var patternFile: TelegramMediaFile? + private var patternFileDisposable: Disposable? + private var patternImage: UIImage? + private var patternImageDisposable: Disposable? + + override public init(frame: CGRect) { + self.backgroundView = UIView() + self.avatarBackgroundPatternContainer = UIView() + self.avatarBackgroundGradientLayer = SimpleGradientLayer() + self.avatarBackgroundPatternView = UIView() + self.backgroundPatternContainer = UIView() + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + self.addSubview(self.avatarBackgroundPatternContainer) + self.avatarBackgroundPatternContainer.layer.addSublayer(self.avatarBackgroundGradientLayer) + self.avatarBackgroundPatternContainer.addSubview(self.avatarBackgroundPatternView) + + self.addSubview(self.backgroundPatternContainer) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.patternFileDisposable?.dispose() + self.patternImageDisposable?.dispose() + } + + private func loadPatternFromFile() { + guard let component = self.component else { + return + } + guard let patternContentsTarget = self.patternContentsTarget else { + return + } + guard let patternFile = self.patternFile else { + return + } + self.patternImageDisposable = component.context.animationRenderer.loadFirstFrame( + target: patternContentsTarget, + cache: component.context.animationCache, itemId: "reply-pattern-\(patternFile.fileId)", + size: CGSize(width: 64, height: 64), + fetch: animationCacheFetchFile( + postbox: component.context.account.postbox, + userLocation: .other, + userContentType: .sticker, + resource: .media(media: .standalone(media: patternFile), resource: patternFile.resource), + type: AnimationCacheAnimationType(file: patternFile), + keyframeOnly: false, + customColor: .white + ), + completion: { [weak self] _, _ in + guard let self else { + return + } + self.updatePatternLayerImages() + } + ) + } + + private func updatePatternLayerImages() { + let image = self.patternContentsTarget?.contents + for patternContentLayer in self.avatarPatternContentLayers { + patternContentLayer.contents = image + } + } + + func update(component: PeerInfoCoverComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if self.component?.peer?.backgroundEmojiId != component.peer?.backgroundEmojiId { + if let backgroundEmojiId = component.peer?.backgroundEmojiId, backgroundEmojiId != 0 { + if self.patternContentsTarget == nil { + self.patternContentsTarget = PatternContentsTarget(imageUpdated: { [weak self] in + guard let self else { + return + } + self.updatePatternLayerImages() + }) + } + + self.patternFile = nil + self.patternFileDisposable?.dispose() + self.patternFileDisposable = nil + self.patternImageDisposable?.dispose() + + let fileId = backgroundEmojiId + self.patternFileDisposable = (component.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) + |> deliverOnMainQueue).startStrict(next: { [weak self] files in + guard let self else { + return + } + if let file = files[fileId] { + self.patternFile = file + self.loadPatternFromFile() + } + }) + } else { + self.patternContentsTarget = nil + self.patternFileDisposable?.dispose() + self.patternFileDisposable = nil + self.patternFile = nil + } + } + + self.component = component + self.state = state + + let backgroundColor: UIColor + let patternColor: UIColor + if let peer = component.peer, let colors = peer._asPeer().nameColor.flatMap({ component.context.peerNameColors.get($0) }) { + backgroundColor = colors.main.withMultiplied(hue: 1.0, saturation: 0.9, brightness: 0.9) + patternColor = colors.main.withMultiplied(hue: 1.0, saturation: 1.0, brightness: 0.8).withMultipliedAlpha(0.8) + } else { + backgroundColor = .clear + patternColor = .clear + } + + self.backgroundView.backgroundColor = backgroundColor + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -1000.0 + availableSize.height), size: CGSize(width: availableSize.width, height: 1000.0)) + transition.containedViewLayoutTransition.updateFrameAdditive(view: self.backgroundView, frame: backgroundFrame) + + let avatarBackgroundPatternContainerFrame = CGSize(width: 0.0, height: 0.0).centered(around: component.avatarCenter) + transition.containedViewLayoutTransition.updateFrameAdditive(view: self.avatarBackgroundPatternContainer, frame: avatarBackgroundPatternContainerFrame) + transition.containedViewLayoutTransition.updateSublayerTransformScaleAdditive(layer: self.avatarBackgroundPatternContainer.layer, scale: component.avatarScale) + //transition.containedViewLayoutTransition.updateAlpha(layer: self.avatarBackgroundPatternContainer.layer, alpha: 1.0 - component.avatarTransitionFraction) + + //self.avatarBackgroundPatternView.backgroundColor = .yellow + transition.setFrame(view: self.avatarBackgroundPatternView, frame: CGSize(width: 200.0, height: 200.0).centered(around: CGPoint())) + + + let baseAvatarGradientAlpha: CGFloat = 0.8 + let numSteps = 10 + self.avatarBackgroundGradientLayer.colors = (0 ..< 10).map { i in + let step: CGFloat = 1.0 - CGFloat(i) / CGFloat(numSteps - 1) + return UIColor(white: 1.0, alpha: baseAvatarGradientAlpha * pow(step, 3.0)).cgColor + } + self.avatarBackgroundGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) + self.avatarBackgroundGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) + self.avatarBackgroundGradientLayer.type = .radial + transition.setFrame(layer: self.avatarBackgroundGradientLayer, frame: CGSize(width: 260.0, height: 260.0).centered(around: CGPoint())) + transition.setAlpha(layer: self.avatarBackgroundGradientLayer, alpha: 1.0 - component.avatarTransitionFraction) + + let backgroundPatternContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height), size: CGSize(width: availableSize.width, height: 0.0)) + transition.containedViewLayoutTransition.updateFrameAdditive(view: self.backgroundPatternContainer, frame: backgroundPatternContainerFrame) + if component.peer?.id == component.context.account.peerId { + transition.setAlpha(view: self.backgroundPatternContainer, alpha: 0.0) + } else { + transition.setAlpha(view: self.backgroundPatternContainer, alpha: component.patternTransitionFraction) + } + + var avatarBackgroundPatternLayerCount = 0 + let lokiRng = LokiRng(seed0: 123, seed1: 0, seed2: 0) + for row in 0 ..< 4 { + let avatarPatternCount = row % 2 == 0 ? 9 : 9 + let avatarPatternAngleSpan: CGFloat = CGFloat.pi * 2.0 / CGFloat(avatarPatternCount - 1) + + for i in 0 ..< avatarPatternCount - 1 { + let baseItemDistance: CGFloat = 72.0 + CGFloat(row) * 28.0 + + let itemDistanceFraction = max(0.0, min(1.0, baseItemDistance / 140.0)) + let itemScaleFraction = patternScaleValueAt(fraction: component.avatarTransitionFraction, t: itemDistanceFraction, reverse: false) + let itemDistance = baseItemDistance * (1.0 - itemScaleFraction) + 20.0 * itemScaleFraction + + var itemAngle = -CGFloat.pi * 0.5 + CGFloat(i) * avatarPatternAngleSpan + if row % 2 != 0 { + itemAngle += avatarPatternAngleSpan * 0.5 + } + let itemPosition = CGPoint(x: cos(itemAngle) * itemDistance, y: sin(itemAngle) * itemDistance) + + let itemScale: CGFloat = 0.7 + CGFloat(lokiRng.next()) * (1.0 - 0.7) + let itemSize: CGFloat = floor(26.0 * itemScale) + let itemFrame = CGSize(width: itemSize, height: itemSize).centered(around: itemPosition) + + let itemLayer: SimpleLayer + if self.avatarPatternContentLayers.count > avatarBackgroundPatternLayerCount { + itemLayer = self.avatarPatternContentLayers[avatarBackgroundPatternLayerCount] + } else { + itemLayer = SimpleLayer() + itemLayer.contents = self.patternContentsTarget?.contents + self.avatarBackgroundPatternContainer.layer.addSublayer(itemLayer) + self.avatarPatternContentLayers.append(itemLayer) + } + + itemLayer.frame = itemFrame + itemLayer.layerTintColor = patternColor.cgColor + transition.setAlpha(layer: itemLayer, alpha: (1.0 - CGFloat(row) / 5.0) * (1.0 - itemScaleFraction)) + + avatarBackgroundPatternLayerCount += 1 + } + } + if avatarBackgroundPatternLayerCount > self.avatarPatternContentLayers.count { + for i in avatarBackgroundPatternLayerCount ..< self.avatarPatternContentLayers.count { + self.avatarPatternContentLayers[i].removeFromSuperlayer() + } + self.avatarPatternContentLayers.removeSubrange(avatarBackgroundPatternLayerCount ..< self.avatarPatternContentLayers.count) + } + + /*let patternSpanX: CGFloat = 82.0 + let patternSpanY: CGFloat = 71.0 + let patternHeight: CGFloat = 86.0 + + var backgroundPatternCount = 0 + var patternY: CGFloat = -patternHeight + var patternRowIndex = 0 + while true { + if patternY >= 50.0 { + break + } + + var offsetFromCenter: CGFloat = patternRowIndex % 2 == 0 ? 0.0 : patternSpanX * 0.5 + while true { + if offsetFromCenter >= availableSize.width * 0.5 + 50.0 { + break + } + + for i in 0 ..< (offsetFromCenter == 0.0 ? 1 : 2) { + let itemPosition = CGPoint(x: availableSize.width * 0.5 + (i == 0 ? -1.0 : 1.0) * offsetFromCenter, y: patternY) + let itemLayer: SimpleLayer + if self.backgroundPatternContentLayers.count > backgroundPatternCount { + itemLayer = self.backgroundPatternContentLayers[backgroundPatternCount] + } else { + itemLayer = SimpleLayer() + itemLayer.contents = self.patternContentsTarget?.contents + self.backgroundPatternContainer.layer.addSublayer(itemLayer) + self.backgroundPatternContentLayers.append(itemLayer) + } + + let itemFrame = CGSize(width: 24.0, height: 24.0).centered(around: itemPosition) + itemLayer.frame = itemFrame + itemLayer.layerTintColor = patternColor.cgColor + + backgroundPatternCount += 1 + } + + offsetFromCenter += patternSpanX + } + patternY += patternSpanY + patternRowIndex += 1 + } + if backgroundPatternCount > self.backgroundPatternContentLayers.count { + for i in backgroundPatternCount ..< self.backgroundPatternContentLayers.count { + self.backgroundPatternContentLayers[i].removeFromSuperlayer() + } + self.backgroundPatternContentLayers.removeSubrange(backgroundPatternCount ..< self.backgroundPatternContentLayers.count) + }*/ + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index 63538570fa..63f6ad30ec 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -128,6 +128,7 @@ swift_library( "//submodules/SoftwareVideo", "//submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode", "//submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode", + "//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift new file mode 100644 index 0000000000..75dfe458b3 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift @@ -0,0 +1,133 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import TelegramPresentationData +import AnimationUI +import Display + +class DynamicIslandMaskNode: ASDisplayNode { + var animationNode: AnimationNode? + + var isForum = false { + didSet { + if self.isForum != oldValue { + self.animationNode?.removeFromSupernode() + let animationNode = AnimationNode(animation: "ForumAvatarMask") + self.addSubnode(animationNode) + self.animationNode = animationNode + } + } + } + + override init() { + let animationNode = AnimationNode(animation: "UserAvatarMask") + self.animationNode = animationNode + + super.init() + + self.addSubnode(animationNode) + } + + func update(_ value: CGFloat) { + self.animationNode?.setProgress(value) + } + + var animating = false + + override func layout() { + self.animationNode?.frame = self.bounds + } +} + +class DynamicIslandBlurNode: ASDisplayNode { + private var effectView: UIVisualEffectView? + private let fadeNode = ASDisplayNode() + let gradientNode = ASImageNode() + + private var hierarchyTrackingNode: HierarchyTrackingNode? + + deinit { + self.animator?.stopAnimation(true) + } + + override func didLoad() { + super.didLoad() + + let hierarchyTrackingNode = HierarchyTrackingNode({ [weak self] value in + if !value { + self?.animator?.stopAnimation(true) + self?.animator = nil + } + }) + self.hierarchyTrackingNode = hierarchyTrackingNode + self.addSubnode(hierarchyTrackingNode) + + self.fadeNode.backgroundColor = .black + self.fadeNode.alpha = 0.0 + + self.gradientNode.displaysAsynchronously = false + let gradientImage = generateImage(CGSize(width: 100.0, height: 100.0), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + var locations: [CGFloat] = [0.0, 0.87, 1.0] + let colors: [CGColor] = [UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 1.0).cgColor] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + let endRadius: CGFloat = 90.0 + let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0 + 38.0) + context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endRadius, options: .drawsAfterEndLocation) + }) + self.gradientNode.image = gradientImage + + let effectView = UIVisualEffectView(effect: nil) + self.effectView = effectView + self.view.insertSubview(effectView, at: 0) + + self.addSubnode(self.gradientNode) + self.addSubnode(self.fadeNode) + } + + private var animator: UIViewPropertyAnimator? + + func prepare() -> Bool { + guard self.animator == nil else { + return false + } + let animator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) + self.animator = animator + self.effectView?.effect = nil + animator.addAnimations { [weak self] in + self?.effectView?.effect = UIBlurEffect(style: .dark) + } + return true + } + + func update(_ value: CGFloat) { + let fadeAlpha = min(1.0, max(0.0, -0.25 + value * 1.55)) + if value > 0.0 { + var value = value + let updated = self.prepare() + if value > 0.99 && updated { + value = 0.99 + } + self.animator?.fractionComplete = max(0.0, -0.1 + value * 1.1) + } else { + self.animator?.stopAnimation(true) + self.animator = nil + self.effectView?.effect = nil + } + self.fadeNode.alpha = fadeAlpha + } + + override func layout() { + super.layout() + + self.effectView?.frame = self.bounds + self.fadeNode.frame = self.bounds + + let gradientSize = CGSize(width: 100.0, height: 100.0) + self.gradientNode.frame = CGRect(origin: CGPoint(x: (self.bounds.width - gradientSize.width) / 2.0, y: 0.0), size: gradientSize) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarListNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarListNode.swift new file mode 100644 index 0000000000..d614f08194 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarListNode.swift @@ -0,0 +1,177 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import TelegramPresentationData +import PeerInfoAvatarListNode +import SwiftSignalKit +import Postbox +import TelegramCore +import ContextUI +import AccountContext +import Display + +final class PeerInfoAvatarListNode: ASDisplayNode { + private let isSettings: Bool + let containerNode: ASDisplayNode + let pinchSourceNode: PinchSourceContainerNode + let bottomCoverNode: ASDisplayNode + let maskNode: DynamicIslandMaskNode + let topCoverNode: DynamicIslandBlurNode + let avatarContainerNode: PeerInfoAvatarTransformContainerNode + let listContainerTransformNode: ASDisplayNode + let listContainerNode: PeerInfoAvatarListContainerNode + + let isReady = Promise() + + var arguments: (Peer?, Int64?, EngineMessageHistoryThread.Info?, PresentationTheme, CGFloat, Bool)? + var item: PeerInfoAvatarListItem? + + var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)? + var animateOverlaysFadeIn: (() -> Void)? + var openStories: (() -> Void)? + + init(context: AccountContext, readyWhenGalleryLoads: Bool, isSettings: Bool) { + self.isSettings = isSettings + + self.containerNode = ASDisplayNode() + + self.bottomCoverNode = ASDisplayNode() + + self.maskNode = DynamicIslandMaskNode() + self.pinchSourceNode = PinchSourceContainerNode() + + self.avatarContainerNode = PeerInfoAvatarTransformContainerNode(context: context) + self.listContainerTransformNode = ASDisplayNode() + self.listContainerNode = PeerInfoAvatarListContainerNode(context: context, isSettings: isSettings) + self.listContainerNode.clipsToBounds = true + self.listContainerNode.isHidden = true + + self.topCoverNode = DynamicIslandBlurNode() + + super.init() + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.bottomCoverNode) + self.containerNode.addSubnode(self.pinchSourceNode) + self.pinchSourceNode.contentNode.addSubnode(self.avatarContainerNode) + self.listContainerTransformNode.addSubnode(self.listContainerNode) + self.pinchSourceNode.contentNode.addSubnode(self.listContainerTransformNode) + self.containerNode.addSubnode(self.topCoverNode) + + let avatarReady = (self.avatarContainerNode.avatarNode.ready + |> mapToSignal { _ -> Signal in + return .complete() + } + |> then(.single(true))) + + let galleryReady = self.listContainerNode.isReady.get() + |> filter { value in + return value + } + |> take(1) + + let combinedSignal: Signal + if readyWhenGalleryLoads { + combinedSignal = combineLatest(queue: .mainQueue(), + avatarReady, + galleryReady + ) + |> map { lhs, rhs in + return lhs && rhs + } + } else { + combinedSignal = avatarReady + } + + self.isReady.set(combinedSignal + |> filter { value in + return value + } + |> take(1)) + + self.listContainerNode.itemsUpdated = { [weak self] items in + if let strongSelf = self { + strongSelf.item = items.first + strongSelf.itemsUpdated?(items) + if let (peer, threadId, threadInfo, theme, avatarSize, isExpanded) = strongSelf.arguments { + strongSelf.avatarContainerNode.update(peer: peer, threadId: threadId, threadInfo: threadInfo, item: strongSelf.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: strongSelf.isSettings) + } + } + } + + self.pinchSourceNode.activate = { [weak self] sourceNode in + guard let strongSelf = self, let (_, _, _, _, _, isExpanded) = strongSelf.arguments, isExpanded else { + return + } + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + return UIScreen.main.bounds + }) + context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) + + strongSelf.listContainerNode.bottomShadowNode.alpha = 0.0 + } + + self.pinchSourceNode.animatedOut = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.animateOverlaysFadeIn?() + } + + self.listContainerNode.openStories = { [weak self] in + guard let self else { + return + } + self.openStories?() + } + } + + func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, isForum: Bool, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { + self.arguments = (peer, threadId, threadInfo, theme, avatarSize, isExpanded) + self.maskNode.isForum = isForum + self.pinchSourceNode.update(size: size, transition: transition) + self.containerNode.frame = CGRect(origin: CGPoint(), size: size) + self.pinchSourceNode.frame = CGRect(origin: CGPoint(), size: size) + self.avatarContainerNode.update(peer: peer, threadId: threadId, threadInfo: threadInfo, item: self.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: self.isSettings) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.listContainerNode.isHidden { + if let result = self.listContainerNode.view.hitTest(self.view.convert(point, to: self.listContainerNode.view), with: event) { + return result + } + } else { + if let result = self.avatarContainerNode.avatarNode.view.hitTest(self.view.convert(point, to: self.avatarContainerNode.avatarNode.view), with: event) { + return result + } else if let result = self.avatarContainerNode.iconView?.view?.hitTest(self.view.convert(point, to: self.avatarContainerNode.iconView?.view), with: event) { + return result + } + } + + return super.hitTest(point, with: event) + } + + func animateAvatarCollapse(transition: ContainedViewLayoutTransition) { + if let currentItemNode = self.listContainerNode.currentItemNode, case .animated = transition { + if let _ = self.avatarContainerNode.videoNode { + + } else if let _ = self.avatarContainerNode.markupNode { + + } else if let unroundedImage = self.avatarContainerNode.avatarNode.unroundedImage { + let avatarCopyView = UIImageView() + avatarCopyView.image = unroundedImage + avatarCopyView.frame = self.avatarContainerNode.avatarNode.frame + avatarCopyView.center = currentItemNode.imageNode.position + currentItemNode.view.addSubview(avatarCopyView) + let scale = currentItemNode.imageNode.bounds.height / avatarCopyView.bounds.height + avatarCopyView.layer.transform = CATransform3DMakeScale(scale, scale, scale) + avatarCopyView.alpha = 0.0 + transition.updateAlpha(layer: avatarCopyView.layer, alpha: 1.0, completion: { [weak avatarCopyView] _ in + Queue.mainQueue().after(0.1, { + avatarCopyView?.removeFromSuperview() + }) + }) + } + } + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift new file mode 100644 index 0000000000..a14769f9df --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift @@ -0,0 +1,415 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import ContextUI +import TelegramPresentationData +import AccountContext +import AvatarNode +import UniversalMediaPlayer +import Display +import ComponentFlow +import UniversalMediaPlayer +import AvatarVideoNode +import SwiftSignalKit +import TelegramUniversalVideoContent +import PeerInfoAvatarListNode +import Postbox +import TelegramCore +import EmojiStatusComponent +import GalleryUI + + +final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { + let context: AccountContext + + let containerNode: ContextControllerSourceNode + + let avatarNode: AvatarNode + private(set) var avatarStoryView: ComponentView? + var videoNode: UniversalVideoNode? + var markupNode: AvatarVideoNode? + var iconView: ComponentView? + private var videoContent: NativeVideoContent? + private var videoStartTimestamp: Double? + + var isExpanded: Bool = false + var canAttachVideo: Bool = true { + didSet { + if oldValue != self.canAttachVideo { + self.videoNode?.canAttachContent = !self.isExpanded && self.canAttachVideo + } + } + } + + var tapped: (() -> Void)? + var emojiTapped: (() -> Void)? + var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? + + private var isFirstAvatarLoading = true + var item: PeerInfoAvatarListItem? + + private let playbackStartDisposable = MetaDisposable() + + var storyData: (totalCount: Int, unseenCount: Int, hasUnseenCloseFriends: Bool)? + var storyProgress: Float? + + init(context: AccountContext) { + self.context = context + self.containerNode = ContextControllerSourceNode() + + let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0)) + self.avatarNode = AvatarNode(font: avatarFont) + + super.init() + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.avatarNode) + self.containerNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0)) + self.avatarNode.frame = self.containerNode.bounds + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + self.avatarNode.view.addGestureRecognizer(tapGestureRecognizer) + + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self else { + return + } + tapGestureRecognizer.isEnabled = false + tapGestureRecognizer.isEnabled = true + strongSelf.contextAction?(strongSelf.containerNode, gesture) + } + } + + deinit { + self.playbackStartDisposable.dispose() + } + + func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme) { + var colors = AvatarNode.Colors(theme: theme) + colors.seenColors = [ + theme.list.controlSecondaryColor, + theme.list.controlSecondaryColor + ] + var storyStats: AvatarNode.StoryStats? + if let storyData = self.storyData { + storyStats = AvatarNode.StoryStats( + totalCount: storyData.totalCount, + unseenCount: storyData.unseenCount, + hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends, + progress: self.storyProgress + ) + } else if let storyProgress = self.storyProgress { + storyStats = AvatarNode.StoryStats( + totalCount: 1, + unseenCount: 1, + hasUnseenCloseFriendsItems: false, + progress: storyProgress + ) + } + self.avatarNode.setStoryStats(storyStats: storyStats, presentationParams: AvatarNode.StoryPresentationParams( + colors: colors, + lineWidth: 3.0, + inactiveLineWidth: 1.5 + ), transition: Transition(transition)) + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.tapped?() + } + } + + @objc private func emojiTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.emojiTapped?() + } + } + + func updateTransitionFraction(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) { + if let videoNode = self.videoNode { + if case .immediate = transition, fraction == 1.0 { + return + } + if fraction > 0.0 { + videoNode.pause() + } else { + videoNode.play() + } + transition.updateAlpha(node: videoNode, alpha: 1.0 - fraction) + } + if let markupNode = self.markupNode { + if case .immediate = transition, fraction == 1.0 { + return + } + if fraction > 0.0 { + markupNode.updateVisibility(false) + } else { + markupNode.updateVisibility(true) + } + transition.updateAlpha(node: markupNode, alpha: 1.0 - fraction) + } + } + + var removedPhotoResourceIds = Set() + func update(peer: Peer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) { + if let peer = peer { + let previousItem = self.item + var item = item + self.item = item + + var overrideImage: AvatarNodeImageOverride? + if peer.isDeleted { + overrideImage = .deletedIcon + } else if let previousItem = previousItem, item == nil { + if case let .image(_, representations, _, _, _, _) = previousItem, let rep = representations.last { + self.removedPhotoResourceIds.insert(rep.representation.resource.id.stringRepresentation) + } + overrideImage = AvatarNodeImageOverride.none + item = nil + } else if let rep = peer.profileImageRepresentations.last, self.removedPhotoResourceIds.contains(rep.resource.id.stringRepresentation) { + overrideImage = AvatarNodeImageOverride.none + item = nil + } + + if let _ = overrideImage { + self.containerNode.isGestureEnabled = false + } else if peer.profileImageRepresentations.isEmpty { + self.containerNode.isGestureEnabled = false + } else { + self.containerNode.isGestureEnabled = false + } + + self.avatarNode.imageNode.animateFirstTransition = !isSettings + self.avatarNode.setPeer(context: self.context, theme: theme, peer: EnginePeer(peer), overrideImage: overrideImage, clipStyle: .none, synchronousLoad: self.isFirstAvatarLoading, displayDimensions: CGSize(width: avatarSize, height: avatarSize), storeUnrounded: true) + + if let threadInfo = threadInfo { + self.avatarNode.isHidden = true + + let iconView: ComponentView + if let current = self.iconView { + iconView = current + } else { + iconView = ComponentView() + self.iconView = iconView + } + let content: EmojiStatusComponent.Content + if threadId == 1 { + content = .image(image: PresentationResourcesChat.chatGeneralThreadIcon(theme)) + } else if let iconFileId = threadInfo.icon { + content = .animation(content: .customEmoji(fileId: iconFileId), size: CGSize(width: avatarSize, height: avatarSize), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .forever) + } else { + content = .topic(title: String(threadInfo.title.prefix(1)), color: threadInfo.iconColor, size: CGSize(width: avatarSize, height: avatarSize)) + } + let _ = iconView.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.context.animationCache, + animationRenderer: self.context.animationRenderer, + content: content, + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: CGSize(width: avatarSize, height: avatarSize) + ) + if let iconComponentView = iconView.view { + iconComponentView.isUserInteractionEnabled = true + if iconComponentView.superview == nil { + iconComponentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.emojiTapGesture(_:)))) + self.avatarNode.view.superview?.addSubview(iconComponentView) + } + iconComponentView.frame = CGRect(origin: CGPoint(), size: CGSize(width: avatarSize, height: avatarSize)) + } + } + + var isForum = false + let avatarCornerRadius: CGFloat + if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { + avatarCornerRadius = floor(avatarSize * 0.25) + isForum = true + } else { + avatarCornerRadius = avatarSize / 2.0 + } + if self.avatarNode.layer.cornerRadius != 0.0 { + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut).updateCornerRadius(layer: self.avatarNode.contentNode.layer, cornerRadius: avatarCornerRadius) + } else { + self.avatarNode.contentNode.layer.cornerRadius = avatarCornerRadius + } + self.avatarNode.contentNode.layer.masksToBounds = true + + self.isFirstAvatarLoading = false + + self.containerNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize)) + self.avatarNode.frame = self.containerNode.bounds + self.avatarNode.font = avatarPlaceholderFont(size: floor(avatarSize * 16.0 / 37.0)) + + if let item = item { + let representations: [ImageRepresentationWithReference] + let videoRepresentations: [VideoRepresentationWithReference] + let immediateThumbnailData: Data? + var videoId: Int64 + let markup: TelegramMediaImage.EmojiMarkup? + switch item { + case .custom: + representations = [] + videoRepresentations = [] + immediateThumbnailData = nil + videoId = 0 + markup = nil + case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): + representations = topRepresentations + videoRepresentations = videoRepresentationsValue + immediateThumbnailData = immediateThumbnail + videoId = peer.id.id._internalGetInt64Value() + if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { + videoId = videoId &+ resource.photoId + } + markup = nil + case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, markupValue): + representations = imageRepresentations + videoRepresentations = videoRepresentationsValue + immediateThumbnailData = immediateThumbnail + if case let .cloud(imageId, _, _) = reference { + videoId = imageId + } else { + videoId = peer.id.id._internalGetInt64Value() + } + markup = markupValue + } + + self.containerNode.isGestureEnabled = !isSettings + + if let markup { + if let videoNode = self.videoNode { + self.videoContent = nil + self.videoStartTimestamp = nil + self.videoNode = nil + + videoNode.removeFromSupernode() + } + + let markupNode: AvatarVideoNode + if let current = self.markupNode { + markupNode = current + } else { + markupNode = AvatarVideoNode(context: self.context) + self.avatarNode.contentNode.addSubnode(markupNode) + self.markupNode = markupNode + } + markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0)) + markupNode.updateVisibility(true) + } else if threadInfo == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) { + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil)])) + let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil) + if videoContent.id != self.videoContent?.id { + self.videoNode?.removeFromSupernode() + + let mediaManager = self.context.sharedContext.mediaManager + let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .embedded) + videoNode.isUserInteractionEnabled = false + videoNode.isHidden = true + + if let startTimestamp = video.representation.startTimestamp { + self.videoStartTimestamp = startTimestamp + self.playbackStartDisposable.set((videoNode.status + |> map { status -> Bool in + if let status = status, case .playing = status.status { + return true + } else { + return false + } + } + |> filter { playing in + return playing + } + |> take(1) + |> deliverOnMainQueue).start(completed: { [weak self] in + if let strongSelf = self { + Queue.mainQueue().after(0.15) { + strongSelf.videoNode?.isHidden = false + } + } + })) + } else { + self.videoStartTimestamp = nil + self.playbackStartDisposable.set(nil) + videoNode.isHidden = false + } + + self.videoContent = videoContent + self.videoNode = videoNode + + let maskPath: UIBezierPath + if isForum { + maskPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size), cornerRadius: avatarCornerRadius) + } else { + maskPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size)) + } + let shape = CAShapeLayer() + shape.path = maskPath.cgPath + videoNode.layer.mask = shape + + self.avatarNode.contentNode.addSubnode(videoNode) + } + } else { + if let markupNode = self.markupNode { + self.markupNode = nil + markupNode.removeFromSupernode() + } + if let videoNode = self.videoNode { + self.videoStartTimestamp = nil + self.videoContent = nil + self.videoNode = nil + + videoNode.removeFromSupernode() + } + } + } else { + if let markupNode = self.markupNode { + self.markupNode = nil + markupNode.removeFromSupernode() + } + if let videoNode = self.videoNode { + self.videoStartTimestamp = nil + self.videoContent = nil + self.videoNode = nil + + videoNode.removeFromSupernode() + } + self.containerNode.isGestureEnabled = false + } + + if let markupNode = self.markupNode { + markupNode.frame = self.avatarNode.bounds + markupNode.updateLayout(size: self.avatarNode.bounds.size, cornerRadius: avatarCornerRadius, transition: .immediate) + } + + if let videoNode = self.videoNode { + if self.canAttachVideo { + videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate) + } + videoNode.frame = self.avatarNode.contentNode.bounds + + if isExpanded == videoNode.canAttachContent { + self.isExpanded = isExpanded + let update = { + videoNode.canAttachContent = !self.isExpanded && self.canAttachVideo + if videoNode.canAttachContent { + videoNode.play() + } + } + if isExpanded { + DispatchQueue.main.async { + update() + } + } else { + update() + } + } + } + } + + self.updateStoryView(transition: .immediate, theme: theme) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift new file mode 100644 index 0000000000..909f535749 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift @@ -0,0 +1,233 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import TelegramPresentationData +import AccountContext +import AvatarNode +import UniversalMediaPlayer +import PeerInfoAvatarListNode +import AvatarVideoNode +import TelegramUniversalVideoContent +import SwiftSignalKit +import Postbox +import TelegramCore +import Display +import GalleryUI + +final class PeerInfoEditingAvatarNode: ASDisplayNode { + private let context: AccountContext + let avatarNode: AvatarNode + fileprivate var videoNode: UniversalVideoNode? + fileprivate var markupNode: AvatarVideoNode? + private var videoContent: NativeVideoContent? + private var videoStartTimestamp: Double? + var item: PeerInfoAvatarListItem? + + var tapped: ((Bool) -> Void)? + + var canAttachVideo: Bool = true + + init(context: AccountContext) { + self.context = context + let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0)) + self.avatarNode = AvatarNode(font: avatarFont) + + super.init() + + self.addSubnode(self.avatarNode) + self.avatarNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0)) + + self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.tapped?(false) + } + } + + func reset() { + guard let videoNode = self.videoNode else { + return + } + videoNode.isHidden = true + videoNode.seek(self.videoStartTimestamp ?? 0.0) + Queue.mainQueue().after(0.15) { + videoNode.isHidden = false + } + } + + var removedPhotoResourceIds = Set() + func update(peer: Peer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, item: PeerInfoAvatarListItem?, updatingAvatar: PeerInfoUpdatingAvatar?, uploadProgress: AvatarUploadProgress?, theme: PresentationTheme, avatarSize: CGFloat, isEditing: Bool) { + guard let peer = peer else { + return + } + + let canEdit = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) + + let previousItem = self.item + var item = item + self.item = item + + let overrideImage: AvatarNodeImageOverride? + if canEdit, peer.profileImageRepresentations.isEmpty { + overrideImage = .editAvatarIcon(forceNone: true) + } else if let previousItem = previousItem, item == nil { + if case let .image(_, representations, _, _, _, _) = previousItem, let rep = representations.last { + self.removedPhotoResourceIds.insert(rep.representation.resource.id.stringRepresentation) + } + overrideImage = canEdit ? .editAvatarIcon(forceNone: true) : AvatarNodeImageOverride.none + item = nil + } else if let representation = peer.profileImageRepresentations.last, self.removedPhotoResourceIds.contains(representation.resource.id.stringRepresentation) { + overrideImage = canEdit ? .editAvatarIcon(forceNone: true) : AvatarNodeImageOverride.none + item = nil + } else { + overrideImage = item == nil && canEdit ? .editAvatarIcon(forceNone: true) : nil + } + self.avatarNode.font = avatarPlaceholderFont(size: floor(avatarSize * 16.0 / 37.0)) + self.avatarNode.setPeer(context: self.context, theme: theme, peer: EnginePeer(peer), overrideImage: overrideImage, clipStyle: .none, synchronousLoad: false, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + self.avatarNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize)) + + var isForum = false + let avatarCornerRadius: CGFloat + if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { + isForum = true + avatarCornerRadius = floor(avatarSize * 0.25) + } else { + avatarCornerRadius = avatarSize / 2.0 + } + if self.avatarNode.layer.cornerRadius != 0.0 { + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut).updateCornerRadius(layer: self.avatarNode.layer, cornerRadius: avatarCornerRadius) + } else { + self.avatarNode.layer.cornerRadius = avatarCornerRadius + } + self.avatarNode.layer.masksToBounds = true + + if let item = item { + let representations: [ImageRepresentationWithReference] + let videoRepresentations: [VideoRepresentationWithReference] + let immediateThumbnailData: Data? + var videoId: Int64 + let markup: TelegramMediaImage.EmojiMarkup? + switch item { + case .custom: + representations = [] + videoRepresentations = [] + immediateThumbnailData = nil + videoId = 0 + markup = nil + case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): + representations = topRepresentations + videoRepresentations = videoRepresentationsValue + immediateThumbnailData = immediateThumbnail + videoId = peer.id.id._internalGetInt64Value() + if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { + videoId = videoId &+ resource.photoId + } + markup = nil + case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, markupValue): + representations = imageRepresentations + videoRepresentations = videoRepresentationsValue + immediateThumbnailData = immediateThumbnail + if case let .cloud(imageId, _, _) = reference { + videoId = imageId + } else { + videoId = peer.id.id._internalGetInt64Value() + } + markup = markupValue + } + + if let markup { + if let videoNode = self.videoNode { + self.videoContent = nil + self.videoStartTimestamp = nil + self.videoNode = nil + + videoNode.removeFromSupernode() + } + + let markupNode: AvatarVideoNode + if let current = self.markupNode { + markupNode = current + } else { + markupNode = AvatarVideoNode(context: self.context) + self.avatarNode.contentNode.addSubnode(markupNode) + self.markupNode = markupNode + } + markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0)) + markupNode.updateVisibility(true) + } else if threadData == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) { + if let markupNode = self.markupNode { + self.markupNode = nil + markupNode.removeFromSupernode() + } + + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil)])) + let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil) + if videoContent.id != self.videoContent?.id { + self.videoNode?.removeFromSupernode() + + let mediaManager = self.context.sharedContext.mediaManager + let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .gallery) + videoNode.isUserInteractionEnabled = false + self.videoStartTimestamp = video.representation.startTimestamp + self.videoContent = videoContent + self.videoNode = videoNode + + let maskPath: UIBezierPath + if isForum { + maskPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size), cornerRadius: avatarCornerRadius) + } else { + maskPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size)) + } + let shape = CAShapeLayer() + shape.path = maskPath.cgPath + videoNode.layer.mask = shape + + self.avatarNode.contentNode.addSubnode(videoNode) + } + } else { + if let markupNode = self.markupNode { + self.markupNode = nil + markupNode.removeFromSupernode() + } + if let videoNode = self.videoNode { + self.videoStartTimestamp = nil + self.videoContent = nil + self.videoNode = nil + + videoNode.removeFromSupernode() + } + } + } else if let videoNode = self.videoNode { + self.videoStartTimestamp = nil + self.videoContent = nil + self.videoNode = nil + + videoNode.removeFromSupernode() + } + + if let markupNode = self.markupNode { + markupNode.frame = self.avatarNode.bounds + markupNode.updateLayout(size: self.avatarNode.bounds.size, cornerRadius: avatarCornerRadius, transition: .immediate) + } + + if let videoNode = self.videoNode { + if self.canAttachVideo { + videoNode.updateLayout(size: self.avatarNode.bounds.size, transition: .immediate) + } + videoNode.frame = self.avatarNode.bounds + + if isEditing != videoNode.canAttachContent { + videoNode.canAttachContent = isEditing && self.canAttachVideo + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.avatarNode.frame.contains(point) { + return self.avatarNode.view + } + return super.hitTest(point, with: event) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarOverlayNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarOverlayNode.swift new file mode 100644 index 0000000000..b585b19b98 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarOverlayNode.swift @@ -0,0 +1,149 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import TelegramPresentationData +import AccountContext +import Display +import RadialStatusNode +import Postbox +import TelegramCore +import PeerInfoAvatarListNode +import AvatarNode +import SwiftSignalKit + +final class PeerInfoEditingAvatarOverlayNode: ASDisplayNode { + private let context: AccountContext + + private let imageNode: ImageNode + private let updatingAvatarOverlay: ASImageNode + private let iconNode: ASImageNode + private var statusNode: RadialStatusNode + + private var currentRepresentation: TelegramMediaImageRepresentation? + + init(context: AccountContext) { + self.context = context + + self.imageNode = ImageNode(enableEmpty: true) + + self.updatingAvatarOverlay = ASImageNode() + self.updatingAvatarOverlay.displayWithoutProcessing = true + self.updatingAvatarOverlay.displaysAsynchronously = false + self.updatingAvatarOverlay.alpha = 0.0 + + self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.6)) + self.statusNode.isUserInteractionEnabled = false + + self.iconNode = ASImageNode() + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIconLarge"), color: .white) + self.iconNode.alpha = 0.0 + + super.init() + + self.imageNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0)) + self.updatingAvatarOverlay.frame = self.imageNode.frame + + let radialStatusSize: CGFloat = 50.0 + let imagePosition = self.imageNode.position + self.statusNode.frame = CGRect(origin: CGPoint(x: floor(imagePosition.x - radialStatusSize / 2.0), y: floor(imagePosition.y - radialStatusSize / 2.0)), size: CGSize(width: radialStatusSize, height: radialStatusSize)) + + if let image = self.iconNode.image { + self.iconNode.frame = CGRect(origin: CGPoint(x: floor(imagePosition.x - image.size.width / 2.0), y: floor(imagePosition.y - image.size.height / 2.0)), size: image.size) + } + + self.addSubnode(self.imageNode) + self.addSubnode(self.updatingAvatarOverlay) + self.addSubnode(self.statusNode) + } + + func updateTransitionFraction(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updateAlpha(node: self, alpha: 1.0 - fraction) + } + + func update(peer: Peer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, item: PeerInfoAvatarListItem?, updatingAvatar: PeerInfoUpdatingAvatar?, uploadProgress: AvatarUploadProgress?, theme: PresentationTheme, avatarSize: CGFloat, isEditing: Bool) { + guard let peer = peer else { + return + } + + self.imageNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize)) + self.updatingAvatarOverlay.frame = self.imageNode.frame + + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear) + + let clipStyle: AvatarNodeClipStyle + if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { + clipStyle = .roundedRect + } else { + clipStyle = .round + } + + var isPersonal = false + if let updatingAvatar, case let .image(image) = updatingAvatar, image.isPersonal { + isPersonal = true + } + + if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) + || isPersonal + || self.currentRepresentation != nil && updatingAvatar == nil { + var overlayHidden = true + if let updatingAvatar = updatingAvatar { + overlayHidden = false + + var cancelEnabled = true + let progressValue: CGFloat? + if let uploadProgress { + switch uploadProgress { + case let .value(value): + progressValue = max(0.027, value) + case .indefinite: + progressValue = nil + cancelEnabled = false + } + } else { + progressValue = 0.027 + } + self.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: progressValue, cancelEnabled: cancelEnabled, animateRotation: true)) + + if case let .image(representation) = updatingAvatar { + if representation != self.currentRepresentation { + self.currentRepresentation = representation + + if let signal = peerAvatarImage(account: context.account, peerReference: nil, authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: avatarSize, height: avatarSize), clipStyle: clipStyle, emptyColor: nil, synchronousLoad: false, provideUnrounded: false) { + self.imageNode.setSignal(signal |> map { $0?.0 }) + } + } + } + + transition.updateAlpha(node: self.updatingAvatarOverlay, alpha: 1.0) + } else { + let targetOverlayAlpha: CGFloat = 0.0 + if self.updatingAvatarOverlay.alpha != targetOverlayAlpha { + let update = { + self.statusNode.transitionToState(.none) + self.currentRepresentation = nil + self.imageNode.setSignal(.single(nil)) + transition.updateAlpha(node: self.updatingAvatarOverlay, alpha: overlayHidden ? 0.0 : 1.0) + } + Queue.mainQueue().after(0.3) { + update() + } + } + } + if !overlayHidden && self.updatingAvatarOverlay.image == nil { + switch clipStyle { + case .round: + self.updatingAvatarOverlay.image = generateFilledCircleImage(diameter: avatarSize, color: UIColor(white: 0.0, alpha: 0.4), backgroundColor: nil) + case .roundedRect: + self.updatingAvatarOverlay.image = generateFilledRoundedRectImage(size: CGSize(width: avatarSize, height: avatarSize), cornerRadius: avatarSize * 0.25, color: UIColor(white: 0.0, alpha: 0.4), backgroundColor: nil) + default: + break + } + } + } else { + self.statusNode.transitionToState(.none) + self.currentRepresentation = nil + transition.updateAlpha(node: self.iconNode, alpha: 0.0) + transition.updateAlpha(node: self.updatingAvatarOverlay, alpha: 0.0) + } + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderActionButtonNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderActionButtonNode.swift new file mode 100644 index 0000000000..e71cf81097 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderActionButtonNode.swift @@ -0,0 +1,88 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import ContextUI +import TelegramPresentationData +import Display + +final class PeerInfoHeaderActionButtonNode: HighlightableButtonNode { + let key: PeerInfoHeaderButtonKey + private let action: (PeerInfoHeaderActionButtonNode, ContextGesture?) -> Void + let referenceNode: ContextReferenceContentNode + let containerNode: ContextControllerSourceNode + private let backgroundNode: ASDisplayNode + private let textNode: ImmediateTextNode + + private var theme: PresentationTheme? + + init(key: PeerInfoHeaderButtonKey, action: @escaping (PeerInfoHeaderActionButtonNode, ContextGesture?) -> Void) { + self.key = key + self.action = action + + self.referenceNode = ContextReferenceContentNode() + self.containerNode = ContextControllerSourceNode() + self.containerNode.animateScale = false + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.cornerRadius = 11.0 + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.isUserInteractionEnabled = false + + super.init() + + self.accessibilityTraits = .button + + self.containerNode.addSubnode(self.referenceNode) + self.referenceNode.addSubnode(self.backgroundNode) + self.addSubnode(self.containerNode) + self.addSubnode(self.textNode) + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.layer.removeAnimation(forKey: "opacity") + strongSelf.alpha = 0.4 + } else { + strongSelf.alpha = 1.0 + strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.containerNode.activated = { [weak self] gesture, _ in + if let strongSelf = self { + strongSelf.action(strongSelf, gesture) + } + } + + self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + @objc private func buttonPressed() { + self.action(self, nil) + } + + func update(size: CGSize, text: String, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + let themeUpdated = self.theme != presentationData.theme + if themeUpdated { + self.theme = presentationData.theme + + self.containerNode.isGestureEnabled = false + + self.backgroundNode.backgroundColor = presentationData.theme.list.itemAccentColor + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + } + + self.textNode.attributedText = NSAttributedString(string: text, font: Font.semibold(16.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor) + self.accessibilityLabel = text + let titleSize = self.textNode.updateLayout(CGSize(width: 120.0, height: .greatestFiniteMagnitude)) + + transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrameAdditiveToCenter(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)) + + self.referenceNode.frame = self.containerNode.bounds + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderButtonNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderButtonNode.swift new file mode 100644 index 0000000000..defeeb8645 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderButtonNode.swift @@ -0,0 +1,264 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import ContextUI +import AnimationUI +import Display +import TelegramPresentationData + +enum PeerInfoHeaderButtonKey: Hashable { + case message + case discussion + case call + case videoCall + case voiceChat + case mute + case more + case addMember + case search + case leave + case stop + case addContact +} + +enum PeerInfoHeaderButtonIcon { + case message + case call + case videoCall + case voiceChat + case mute + case unmute + case more + case addMember + case search + case leave + case stop +} + +final class PeerInfoHeaderButtonNode: HighlightableButtonNode { + let key: PeerInfoHeaderButtonKey + private let action: (PeerInfoHeaderButtonNode, ContextGesture?) -> Void + let referenceNode: ContextReferenceContentNode + let containerNode: ContextControllerSourceNode + private let backgroundNode: NavigationBackgroundNode + private let iconNode: ASImageNode + private let textNode: ImmediateTextNode + private var animationNode: AnimationNode? + + private var theme: PresentationTheme? + private var icon: PeerInfoHeaderButtonIcon? + private var isActive: Bool? + + init(key: PeerInfoHeaderButtonKey, action: @escaping (PeerInfoHeaderButtonNode, ContextGesture?) -> Void) { + self.key = key + self.action = action + + self.referenceNode = ContextReferenceContentNode() + self.containerNode = ContextControllerSourceNode() + self.containerNode.animateScale = false + + self.backgroundNode = NavigationBackgroundNode(color: UIColor(white: 1.0, alpha: 0.2), enableBlur: true, enableSaturation: false) + self.backgroundNode.isUserInteractionEnabled = false + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.iconNode.isUserInteractionEnabled = false + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.isUserInteractionEnabled = false + + super.init() + + self.accessibilityTraits = .button + + self.containerNode.addSubnode(self.referenceNode) + self.referenceNode.addSubnode(self.backgroundNode) + self.referenceNode.addSubnode(self.iconNode) + self.addSubnode(self.containerNode) + self.addSubnode(self.textNode) + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.layer.removeAnimation(forKey: "opacity") + strongSelf.alpha = 0.4 + } else { + strongSelf.alpha = 1.0 + strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.containerNode.activated = { [weak self] gesture, _ in + if let strongSelf = self { + strongSelf.action(strongSelf, gesture) + } + } + + self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + @objc private func buttonPressed() { + switch self.icon { + case .voiceChat, .more, .leave: + self.animationNode?.playOnce() + default: + break + } + self.action(self, nil) + } + + func update(size: CGSize, text: String, icon: PeerInfoHeaderButtonIcon, isActive: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + let previousIcon = self.icon + let themeUpdated = self.theme != presentationData.theme + let iconUpdated = self.icon != icon + let isActiveUpdated = self.isActive != isActive + self.isActive = isActive + + let iconSize = CGSize(width: 40.0, height: 40.0) + + if themeUpdated || iconUpdated { + self.theme = presentationData.theme + self.icon = icon + + var isGestureEnabled = false + if [.mute, .voiceChat, .more].contains(icon) { + isGestureEnabled = true + } + self.containerNode.isGestureEnabled = isGestureEnabled + + let animationName: String? + var colors: [String: UIColor] = [:] + var playOnce = false + var seekToEnd = false + let iconColor = UIColor.white + switch icon { + case .voiceChat: + animationName = "anim_profilevc" + colors = ["Line 3.Group 1.Stroke 1": iconColor, + "Line 1.Group 1.Stroke 1": iconColor, + "Line 2.Group 1.Stroke 1": iconColor] + case .mute: + animationName = "anim_profileunmute" + colors = ["Middle.Group 1.Fill 1": iconColor, + "Top.Group 1.Fill 1": iconColor, + "Bottom.Group 1.Fill 1": iconColor, + "EXAMPLE.Group 1.Fill 1": iconColor, + "Line.Group 1.Stroke 1": iconColor] + if previousIcon == .unmute { + playOnce = true + } else { + seekToEnd = true + } + case .unmute: + animationName = "anim_profilemute" + colors = ["Middle.Group 1.Fill 1": iconColor, + "Top.Group 1.Fill 1": iconColor, + "Bottom.Group 1.Fill 1": iconColor, + "EXAMPLE.Group 1.Fill 1": iconColor, + "Line.Group 1.Stroke 1": iconColor] + if previousIcon == .mute { + playOnce = true + } else { + seekToEnd = true + } + case .more: + animationName = "anim_profilemore" + colors = ["Point 2.Group 1.Fill 1": iconColor, + "Point 3.Group 1.Fill 1": iconColor, + "Point 1.Group 1.Fill 1": iconColor] + case .leave: + animationName = "anim_profileleave" + colors = ["Arrow.Group 2.Stroke 1": iconColor, + "Door.Group 1.Stroke 1": iconColor, + "Arrow.Group 1.Stroke 1": iconColor] + default: + animationName = nil + } + + if let animationName = animationName { + let animationNode: AnimationNode + if let current = self.animationNode { + animationNode = current + animationNode.setAnimation(name: animationName, colors: colors) + } else { + animationNode = AnimationNode(animation: animationName, colors: colors, scale: 1.0) + self.referenceNode.addSubnode(animationNode) + self.animationNode = animationNode + } + } else if let animationNode = self.animationNode { + self.animationNode = nil + animationNode.removeFromSupernode() + } + + if playOnce { + self.animationNode?.play() + } else if seekToEnd { + self.animationNode?.seekToEnd() + } + + //self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor + self.iconNode.image = generateImage(iconSize, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.normal) + context.setFillColor(iconColor.cgColor) + let imageName: String? + switch icon { + case .message: + imageName = "Peer Info/ButtonMessage" + case .call: + imageName = "Peer Info/ButtonCall" + case .videoCall: + imageName = "Peer Info/ButtonVideo" + case .voiceChat: + imageName = nil + case .mute: + imageName = nil + case .unmute: + imageName = nil + case .more: + imageName = nil + case .addMember: + imageName = "Peer Info/ButtonAddMember" + case .search: + imageName = "Peer Info/ButtonSearch" + case .leave: + imageName = nil + case .stop: + imageName = "Peer Info/ButtonStop" + } + if let imageName = imageName, let image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: .white) { + let imageRect = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) + context.clip(to: imageRect, mask: image.cgImage!) + context.fill(imageRect) + } + }) + } + + if isActiveUpdated { + let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + alphaTransition.updateAlpha(node: self.iconNode, alpha: isActive ? 1.0 : 0.3) + if let animationNode = self.animationNode { + alphaTransition.updateAlpha(node: animationNode, alpha: isActive ? 1.0 : 0.3) + } + alphaTransition.updateAlpha(node: self.textNode, alpha: isActive ? 1.0 : 0.3) + } + + self.textNode.attributedText = NSAttributedString(string: text.lowercased(), font: Font.regular(11.0), textColor: .white) + self.accessibilityLabel = text + let titleSize = self.textNode.updateLayout(CGSize(width: 120.0, height: .greatestFiniteMagnitude)) + + transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundNode.update(size: size, cornerRadius: 11.0, transition: transition) + transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: 1.0), size: iconSize)) + if let animationNode = self.animationNode { + transition.updateFrame(node: animationNode, frame: CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: 1.0), size: iconSize)) + } + transition.updateFrameAdditiveToCenter(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: size.height - titleSize.height - 9.0), size: titleSize)) + + self.referenceNode.frame = self.containerNode.bounds + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift new file mode 100644 index 0000000000..4233007ae9 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift @@ -0,0 +1,183 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import TelegramPresentationData +import AccountContext +import Display +import Postbox +import TelegramCore +import SwiftSignalKit + +final class PeerInfoHeaderEditingContentNode: ASDisplayNode { + private let context: AccountContext + private let requestUpdateLayout: () -> Void + + var requestEditing: (() -> Void)? + + let avatarNode: PeerInfoEditingAvatarNode + let avatarTextNode: ImmediateTextNode + let avatarButtonNode: HighlightableButtonNode + + var itemNodes: [PeerInfoHeaderTextFieldNodeKey: PeerInfoHeaderTextFieldNode] = [:] + + init(context: AccountContext, requestUpdateLayout: @escaping () -> Void) { + self.context = context + self.requestUpdateLayout = requestUpdateLayout + + self.avatarNode = PeerInfoEditingAvatarNode(context: context) + + self.avatarTextNode = ImmediateTextNode() + self.avatarButtonNode = HighlightableButtonNode() + + super.init() + + self.addSubnode(self.avatarNode) + self.avatarButtonNode.addSubnode(self.avatarTextNode) + + self.avatarButtonNode.addTarget(self, action: #selector(textPressed), forControlEvents: .touchUpInside) + } + + @objc private func textPressed() { + self.requestEditing?() + } + + func editingTextForKey(_ key: PeerInfoHeaderTextFieldNodeKey) -> String? { + return self.itemNodes[key]?.text + } + + func shakeTextForKey(_ key: PeerInfoHeaderTextFieldNodeKey) { + self.itemNodes[key]?.layer.addShakeAnimation() + } + + func update(width: CGFloat, safeInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, peer: Peer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, cachedData: CachedPeerData?, isContact: Bool, isSettings: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat { + let avatarSize: CGFloat = isModalOverlay ? 200.0 : 100.0 + let avatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize) / 2.0), y: statusBarHeight + 22.0), size: CGSize(width: avatarSize, height: avatarSize)) + transition.updateFrameAdditiveToCenter(node: self.avatarNode, frame: CGRect(origin: avatarFrame.center, size: CGSize())) + + var contentHeight: CGFloat = statusBarHeight + 10.0 + avatarSize + 20.0 + + if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) { + if self.avatarButtonNode.supernode == nil { + self.addSubnode(self.avatarButtonNode) + } + self.avatarTextNode.attributedText = NSAttributedString(string: presentationData.strings.Settings_SetNewProfilePhotoOrVideo, font: Font.regular(17.0), textColor: presentationData.theme.list.itemAccentColor) + self.avatarButtonNode.accessibilityLabel = self.avatarTextNode.attributedText?.string + + let avatarTextSize = self.avatarTextNode.updateLayout(CGSize(width: width, height: 32.0)) + transition.updateFrame(node: self.avatarTextNode, frame: CGRect(origin: CGPoint(), size: avatarTextSize)) + transition.updateFrame(node: self.avatarButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - avatarTextSize.width) / 2.0), y: contentHeight - 1.0), size: avatarTextSize)) + contentHeight += 32.0 + } + + var isEditableBot = false + if let user = peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { + isEditableBot = true + } + var fieldKeys: [PeerInfoHeaderTextFieldNodeKey] = [] + if let user = peer as? TelegramUser { + if !user.isDeleted { + fieldKeys.append(.firstName) + if isEditableBot { + fieldKeys.append(.description) + } else if user.botInfo == nil { + fieldKeys.append(.lastName) + } + } + } else if let _ = peer as? TelegramGroup { + fieldKeys.append(.title) + if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) { + fieldKeys.append(.description) + } + } else if let _ = peer as? TelegramChannel { + fieldKeys.append(.title) + if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) { + fieldKeys.append(.description) + } + } + var hasPrevious = false + for key in fieldKeys { + let itemNode: PeerInfoHeaderTextFieldNode + var updateText: String? + if let current = self.itemNodes[key] { + itemNode = current + } else { + var isMultiline = false + switch key { + case .firstName: + if let peer = peer as? TelegramUser { + if let editableBotInfo = (cachedData as? CachedUserData)?.editableBotInfo { + updateText = editableBotInfo.name + } else { + updateText = peer.firstName ?? "" + } + } + case .lastName: + updateText = (peer as? TelegramUser)?.lastName ?? "" + case .title: + updateText = peer?.debugDisplayTitle ?? "" + case .description: + isMultiline = true + if let cachedData = cachedData as? CachedChannelData { + updateText = cachedData.about ?? "" + } else if let cachedData = cachedData as? CachedGroupData { + updateText = cachedData.about ?? "" + } else if let cachedData = cachedData as? CachedUserData { + if let editableBotInfo = cachedData.editableBotInfo { + updateText = editableBotInfo.about + } else { + updateText = cachedData.about ?? "" + } + } else { + updateText = "" + } + } + if isMultiline { + itemNode = PeerInfoHeaderMultiLineTextFieldNode(requestUpdateHeight: { [weak self] in + self?.requestUpdateLayout() + }) + } else { + itemNode = PeerInfoHeaderSingleLineTextFieldNode() + } + self.itemNodes[key] = itemNode + self.addSubnode(itemNode) + } + let placeholder: String + var isEnabled = true + switch key { + case .firstName: + placeholder = isEditableBot ? presentationData.strings.UserInfo_BotNamePlaceholder : presentationData.strings.UserInfo_FirstNamePlaceholder + isEnabled = isContact || isSettings || isEditableBot + case .lastName: + placeholder = presentationData.strings.UserInfo_LastNamePlaceholder + isEnabled = isContact || isSettings + case .title: + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + placeholder = presentationData.strings.GroupInfo_ChannelListNamePlaceholder + } else { + placeholder = presentationData.strings.GroupInfo_GroupNamePlaceholder + } + isEnabled = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) + case .description: + placeholder = presentationData.strings.Channel_Edit_AboutItem + isEnabled = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) || isEditableBot + } + let itemHeight = itemNode.update(width: width, safeInset: safeInset, isSettings: isSettings, hasPrevious: hasPrevious, hasNext: key != fieldKeys.last, placeholder: placeholder, isEnabled: isEnabled, presentationData: presentationData, updateText: updateText) + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight))) + contentHeight += itemHeight + hasPrevious = true + } + var removeKeys: [PeerInfoHeaderTextFieldNodeKey] = [] + for (key, _) in self.itemNodes { + if !fieldKeys.contains(key) { + removeKeys.append(key) + } + } + for key in removeKeys { + if let itemNode = self.itemNodes.removeValue(forKey: key) { + itemNode.removeFromSupernode() + } + } + + return contentHeight + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderMultiLineTextFieldNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderMultiLineTextFieldNode.swift new file mode 100644 index 0000000000..504cf03170 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderMultiLineTextFieldNode.swift @@ -0,0 +1,226 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import ContextUI +import TelegramPresentationData +import Display +import TelegramUIPreferences + +final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode, ASEditableTextNodeDelegate { + private let backgroundNode: ASDisplayNode + private let textNode: EditableTextNode + private let textNodeContainer: ASDisplayNode + private let measureTextNode: ImmediateTextNode + private let clearIconNode: ASImageNode + private let clearButtonNode: HighlightableButtonNode + private let topSeparator: ASDisplayNode + private let maskNode: ASImageNode + + private let requestUpdateHeight: () -> Void + + private var fontSize: PresentationFontSize? + private var theme: PresentationTheme? + private var currentParams: (width: CGFloat, safeInset: CGFloat)? + private var currentMeasuredHeight: CGFloat? + + var text: String { + return self.textNode.attributedText?.string ?? "" + } + + init(requestUpdateHeight: @escaping () -> Void) { + self.requestUpdateHeight = requestUpdateHeight + + self.backgroundNode = ASDisplayNode() + + self.textNode = EditableTextNode() + self.textNode.clipsToBounds = false + self.textNode.textView.clipsToBounds = false + self.textNode.textContainerInset = UIEdgeInsets() + + self.textNodeContainer = ASDisplayNode() + self.measureTextNode = ImmediateTextNode() + self.measureTextNode.maximumNumberOfLines = 0 + self.measureTextNode.isUserInteractionEnabled = false + self.measureTextNode.lineSpacing = 0.1 + self.topSeparator = ASDisplayNode() + + self.clearIconNode = ASImageNode() + self.clearIconNode.isLayerBacked = true + self.clearIconNode.displayWithoutProcessing = true + self.clearIconNode.displaysAsynchronously = false + self.clearIconNode.isHidden = true + + self.clearButtonNode = HighlightableButtonNode() + self.clearButtonNode.isHidden = true + self.clearButtonNode.isAccessibilityElement = false + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + + super.init() + + self.addSubnode(self.backgroundNode) + self.textNodeContainer.addSubnode(self.textNode) + self.addSubnode(self.textNodeContainer) + self.addSubnode(self.clearIconNode) + self.addSubnode(self.clearButtonNode) + self.addSubnode(self.topSeparator) + self.addSubnode(self.maskNode) + + self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside) + self.clearButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.clearIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.clearIconNode.alpha = 0.4 + } else { + strongSelf.clearIconNode.alpha = 1.0 + strongSelf.clearIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + @objc private func clearButtonPressed() { + guard let theme = self.theme else { + return + } + let font: UIFont + if let fontSize = self.fontSize { + font = Font.regular(fontSize.itemListBaseFontSize) + } else { + font = Font.regular(17.0) + } + let attributedText = NSAttributedString(string: "", font: font, textColor: theme.list.itemPrimaryTextColor) + self.textNode.attributedText = attributedText + self.requestUpdateHeight() + self.updateClearButtonVisibility() + } + + func update(width: CGFloat, safeInset: CGFloat, isSettings: Bool, hasPrevious: Bool, hasNext: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat { + self.currentParams = (width, safeInset) + + self.fontSize = presentationData.listsFontSize + let titleFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize) + + if self.theme !== presentationData.theme { + self.theme = presentationData.theme + + self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor + + let textColor = presentationData.theme.list.itemPrimaryTextColor + self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: titleFont, NSAttributedString.Key.foregroundColor.rawValue: textColor] + self.textNode.keyboardAppearance = presentationData.theme.rootController.keyboardColor.keyboardAppearance + self.textNode.tintColor = presentationData.theme.list.itemAccentColor + + self.textNode.clipsToBounds = true + self.textNode.delegate = self + self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + + self.clearIconNode.image = PresentationResourcesItemList.itemListClearInputIcon(presentationData.theme) + } + + self.topSeparator.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let separatorX = safeInset + (hasPrevious ? 16.0 : 0.0) + self.topSeparator.frame = CGRect(origin: CGPoint(x: separatorX, y: 0.0), size: CGSize(width: width - separatorX - safeInset, height: UIScreenPixel)) + + let attributedPlaceholderText = NSAttributedString(string: placeholder, font: titleFont, textColor: presentationData.theme.list.itemPlaceholderTextColor) + if self.textNode.attributedPlaceholderText == nil || !self.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) { + self.textNode.attributedPlaceholderText = attributedPlaceholderText + } + + if let updateText = updateText { + let attributedText = NSAttributedString(string: updateText, font: titleFont, textColor: presentationData.theme.list.itemPrimaryTextColor) + self.textNode.attributedText = attributedText + } + + var measureText = self.textNode.attributedText?.string ?? "" + if measureText.hasSuffix("\n") || measureText.isEmpty { + measureText += "|" + } + let attributedMeasureText = NSAttributedString(string: measureText, font: titleFont, textColor: .gray) + self.measureTextNode.attributedText = attributedMeasureText + let measureTextSize = self.measureTextNode.updateLayout(CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: .greatestFiniteMagnitude)) + self.measureTextNode.frame = CGRect(origin: CGPoint(), size: measureTextSize) + self.currentMeasuredHeight = measureTextSize.height + + let height = measureTextSize.height + 22.0 + + let buttonSize = CGSize(width: 38.0, height: height) + self.clearButtonNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width, y: 0.0), size: buttonSize) + if let image = self.clearIconNode.image { + self.clearIconNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width + floor((buttonSize.width - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size) + } + + let textNodeFrame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: 10.0), size: CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: max(height, 1000.0))) + self.textNodeContainer.frame = textNodeFrame + self.textNode.frame = CGRect(origin: CGPoint(), size: textNodeFrame.size) + + self.backgroundNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: max(1.0, width - safeInset * 2.0), height: height)) + + let hasCorners = safeInset > 0.0 && (!hasPrevious || !hasNext) + let hasTopCorners = hasCorners && !hasPrevious + let hasBottomCorners = hasCorners && !hasNext + + self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + self.maskNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: width - safeInset - safeInset, height: height)) + + return height + } + + func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + self.updateClearButtonVisibility() + } + + func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { + self.updateClearButtonVisibility() + } + + private func updateClearButtonVisibility() { + let isHidden = !self.textNode.isFirstResponder() || self.text.isEmpty + self.clearIconNode.isHidden = isHidden + self.clearButtonNode.isHidden = isHidden + self.clearButtonNode.isAccessibilityElement = isHidden + } + + func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + guard let theme = self.theme else { + return true + } + let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) + if updatedText.count > 255 { + let attributedText = NSAttributedString(string: String(updatedText[updatedText.startIndex.. 0.1 { + self.requestUpdateHeight() + } + } + } + + func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { + let text: String? = UIPasteboard.general.string + if let _ = text { + return true + } else { + return false + } + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift new file mode 100644 index 0000000000..5881239d6d --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift @@ -0,0 +1,286 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import ContextUI +import TelegramPresentationData +import ManagedAnimationNode +import Display + +private enum MoreIconNodeState: Equatable { + case more + case search + case moreToSearch(Float) +} + +private final class MoreIconNode: ManagedAnimationNode { + private let duration: Double = 0.21 + private var iconState: MoreIconNodeState = .more + + init() { + super.init(size: CGSize(width: 30.0, height: 30.0)) + + self.trackTo(item: ManagedAnimationItem(source: .local("anim_moretosearch"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.0)) + } + + func play() { + if case .more = self.iconState { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_moredots"), frames: .range(startFrame: 0, endFrame: 46), duration: 0.76)) + } + } + + func enqueueState(_ state: MoreIconNodeState, animated: Bool) { + guard self.iconState != state else { + return + } + + let previousState = self.iconState + self.iconState = state + + let source = ManagedAnimationSource.local("anim_moretosearch") + + let totalLength: Int = 90 + if animated { + switch previousState { + case .more: + switch state { + case .more: + break + case .search: + self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: totalLength), duration: self.duration)) + case let .moreToSearch(progress): + let frame = Int(progress * Float(totalLength)) + let duration = self.duration * Double(progress) + self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: frame), duration: duration)) + } + case .search: + switch state { + case .more: + self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: 0), duration: self.duration)) + case .search: + break + case let .moreToSearch(progress): + let frame = Int(progress * Float(totalLength)) + let duration = self.duration * Double((1.0 - progress)) + self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: frame), duration: duration)) + } + case let .moreToSearch(currentProgress): + let currentFrame = Int(currentProgress * Float(totalLength)) + switch state { + case .more: + let duration = self.duration * Double(currentProgress) + self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: currentFrame, endFrame: 0), duration: duration)) + case .search: + let duration = self.duration * (1.0 - Double(currentProgress)) + self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: currentFrame, endFrame: totalLength), duration: duration)) + case let .moreToSearch(progress): + let frame = Int(progress * Float(totalLength)) + let duration = self.duration * Double(abs(currentProgress - progress)) + self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: currentFrame, endFrame: frame), duration: duration)) + } + } + } else { + switch state { + case .more: + self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: 0), duration: 0.0)) + case .search: + self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: totalLength), duration: 0.0)) + case let .moreToSearch(progress): + let frame = Int(progress * Float(totalLength)) + self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: frame, endFrame: frame), duration: 0.0)) + } + } + } +} + +final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { + let containerNode: ContextControllerSourceNode + let contextSourceNode: ContextReferenceContentNode + private let regularTextNode: ImmediateTextNode + private let whiteTextNode: ImmediateTextNode + private let iconNode: ASImageNode + private var animationNode: MoreIconNode? + + private var key: PeerInfoHeaderNavigationButtonKey? + private var theme: PresentationTheme? + + var isWhite: Bool = false { + didSet { + if self.isWhite != oldValue { + if case .qrCode = self.key, let theme = self.theme { + self.iconNode.image = self.isWhite ? generateTintedImage(image: PresentationResourcesRootController.navigationQrCodeIcon(theme), color: .white) : PresentationResourcesRootController.navigationQrCodeIcon(theme) + } else if case .postStory = self.key, let theme = self.theme { + self.iconNode.image = self.isWhite ? generateTintedImage(image: PresentationResourcesRootController.navigationPostStoryIcon(theme), color: .white) : PresentationResourcesRootController.navigationPostStoryIcon(theme) + } + + self.regularTextNode.isHidden = self.isWhite + self.whiteTextNode.isHidden = !self.isWhite + self.animationNode?.view.tintColor = self.isWhite ? .white : self.theme?.list.itemAccentColor + self.animationNode?.imageNode.layer.layerTintColor = self.isWhite ? UIColor.white.cgColor : self.theme?.list.itemAccentColor.cgColor + } + } + } + + var action: ((ASDisplayNode, ContextGesture?) -> Void)? + + init() { + self.contextSourceNode = ContextReferenceContentNode() + self.containerNode = ContextControllerSourceNode() + self.containerNode.animateScale = false + + self.regularTextNode = ImmediateTextNode() + self.whiteTextNode = ImmediateTextNode() + self.whiteTextNode.isHidden = true + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + + super.init(pointerStyle: .insetRectangle(-8.0, 2.0)) + + self.isAccessibilityElement = true + self.accessibilityTraits = .button + + self.containerNode.addSubnode(self.contextSourceNode) + self.contextSourceNode.addSubnode(self.regularTextNode) + self.contextSourceNode.addSubnode(self.whiteTextNode) + self.contextSourceNode.addSubnode(self.iconNode) + + self.addSubnode(self.containerNode) + + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self else { + return + } + strongSelf.action?(strongSelf.contextSourceNode, gesture) + } + + self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + } + + @objc private func pressed() { + self.animationNode?.play() + self.action?(self.contextSourceNode, nil) + } + + func update(key: PeerInfoHeaderNavigationButtonKey, presentationData: PresentationData, height: CGFloat) -> CGSize { + let textSize: CGSize + let isFirstTime = self.key == nil + if self.key != key || self.theme !== presentationData.theme { + self.key = key + self.theme = presentationData.theme + + let text: String + var accessibilityText: String + var icon: UIImage? + var isBold = false + var isGestureEnabled = false + var isAnimation = false + var animationState: MoreIconNodeState = .more + switch key { + case .edit: + text = presentationData.strings.Common_Edit + accessibilityText = text + case .done, .cancel, .selectionDone: + text = presentationData.strings.Common_Done + accessibilityText = text + isBold = true + case .select: + text = presentationData.strings.Common_Select + accessibilityText = text + case .search: + text = "" + accessibilityText = presentationData.strings.Common_Search + icon = nil// PresentationResourcesRootController.navigationCompactSearchIcon(presentationData.theme) + isAnimation = true + animationState = .search + case .editPhoto: + text = presentationData.strings.Settings_EditPhoto + accessibilityText = text + case .editVideo: + text = presentationData.strings.Settings_EditVideo + accessibilityText = text + case .more: + text = "" + accessibilityText = presentationData.strings.Common_More + icon = nil// PresentationResourcesRootController.navigationMoreCircledIcon(presentationData.theme) + isGestureEnabled = true + isAnimation = true + animationState = .more + case .qrCode: + text = "" + accessibilityText = presentationData.strings.PeerInfo_QRCode_Title + icon = PresentationResourcesRootController.navigationQrCodeIcon(presentationData.theme) + case .moreToSearch: + text = "" + accessibilityText = "" + case .postStory: + text = "" + accessibilityText = presentationData.strings.Story_Privacy_PostStory + icon = PresentationResourcesRootController.navigationPostStoryIcon(presentationData.theme) + } + self.accessibilityLabel = accessibilityText + self.containerNode.isGestureEnabled = isGestureEnabled + + let font: UIFont = isBold ? Font.semibold(17.0) : Font.regular(17.0) + + self.regularTextNode.attributedText = NSAttributedString(string: text, font: font, textColor: presentationData.theme.rootController.navigationBar.accentTextColor) + self.whiteTextNode.attributedText = NSAttributedString(string: text, font: font, textColor: .white) + self.iconNode.image = icon + + if isAnimation { + self.iconNode.isHidden = true + let animationNode: MoreIconNode + if let current = self.animationNode { + animationNode = current + } else { + animationNode = MoreIconNode() + self.animationNode = animationNode + self.contextSourceNode.addSubnode(animationNode) + } + animationNode.customColor = .white + animationNode.imageNode.layer.layerTintColor = self.isWhite ? UIColor.white.cgColor : presentationData.theme.rootController.navigationBar.accentTextColor.cgColor + animationNode.enqueueState(animationState, animated: !isFirstTime) + } else { + self.iconNode.isHidden = false + if let current = self.animationNode { + self.animationNode = nil + current.removeFromSupernode() + } + } + + textSize = self.regularTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + let _ = self.whiteTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + } else { + textSize = self.regularTextNode.bounds.size + } + + let inset: CGFloat = 0.0 + + let textFrame = CGRect(origin: CGPoint(x: inset, y: floor((height - textSize.height) / 2.0)), size: textSize) + self.regularTextNode.frame = textFrame + self.whiteTextNode.frame = textFrame + + if let animationNode = self.animationNode { + let animationSize = CGSize(width: 30.0, height: 30.0) + + animationNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((height - animationSize.height) / 2.0)), size: animationSize) + + let size = CGSize(width: animationSize.width + inset * 2.0, height: height) + self.containerNode.frame = CGRect(origin: CGPoint(), size: size) + self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size) + return size + } else if let image = self.iconNode.image { + self.iconNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((height - image.size.height) / 2.0)), size: image.size) + + let size = CGSize(width: image.size.width + inset * 2.0, height: height) + self.containerNode.frame = CGRect(origin: CGPoint(), size: size) + self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size) + return size + } else { + let size = CGSize(width: textSize.width + inset * 2.0, height: height) + self.containerNode.frame = CGRect(origin: CGPoint(), size: size) + self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size) + return size + } + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift new file mode 100644 index 0000000000..ae063d16b4 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift @@ -0,0 +1,249 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import ContextUI +import TelegramPresentationData +import Display + +enum PeerInfoHeaderNavigationButtonKey { + case edit + case done + case cancel + case select + case selectionDone + case search + case editPhoto + case editVideo + case more + case qrCode + case moreToSearch + case postStory +} + +struct PeerInfoHeaderNavigationButtonSpec: Equatable { + let key: PeerInfoHeaderNavigationButtonKey + let isForExpandedView: Bool +} + +final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { + private var presentationData: PresentationData? + private(set) var leftButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:] + private(set) var rightButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:] + + private var currentLeftButtons: [PeerInfoHeaderNavigationButtonSpec] = [] + private var currentRightButtons: [PeerInfoHeaderNavigationButtonSpec] = [] + + var isWhite: Bool = false { + didSet { + if self.isWhite != oldValue { + for (_, buttonNode) in self.leftButtonNodes { + buttonNode.isWhite = self.isWhite + } + for (_, buttonNode) in self.rightButtonNodes { + buttonNode.isWhite = self.isWhite + } + } + } + } + + var performAction: ((PeerInfoHeaderNavigationButtonKey, ContextReferenceContentNode?, ContextGesture?) -> Void)? + + func update(size: CGSize, presentationData: PresentationData, leftButtons: [PeerInfoHeaderNavigationButtonSpec], rightButtons: [PeerInfoHeaderNavigationButtonSpec], expandFraction: CGFloat, transition: ContainedViewLayoutTransition) { + let maximumExpandOffset: CGFloat = 14.0 + let expandOffset: CGFloat = -expandFraction * maximumExpandOffset + + if self.currentLeftButtons != leftButtons || presentationData.strings !== self.presentationData?.strings { + self.currentLeftButtons = leftButtons + + var nextRegularButtonOrigin = 16.0 + var nextExpandedButtonOrigin = 16.0 + for spec in leftButtons.reversed() { + let buttonNode: PeerInfoHeaderNavigationButton + var wasAdded = false + if let current = self.leftButtonNodes[spec.key] { + buttonNode = current + } else { + wasAdded = true + buttonNode = PeerInfoHeaderNavigationButton() + self.leftButtonNodes[spec.key] = buttonNode + self.addSubnode(buttonNode) + buttonNode.action = { [weak self] _, gesture in + guard let strongSelf = self, let buttonNode = strongSelf.leftButtonNodes[spec.key] else { + return + } + strongSelf.performAction?(spec.key, buttonNode.contextSourceNode, gesture) + } + } + let buttonSize = buttonNode.update(key: spec.key, presentationData: presentationData, height: size.height) + var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin + let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) + nextButtonOrigin += buttonSize.width + 4.0 + if spec.isForExpandedView { + nextExpandedButtonOrigin = nextButtonOrigin + } else { + nextRegularButtonOrigin = nextButtonOrigin + } + let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) + if wasAdded { + buttonNode.frame = buttonFrame + buttonNode.alpha = 0.0 + transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) + + buttonNode.isWhite = self.isWhite + } else { + transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) + transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) + } + } + var removeKeys: [PeerInfoHeaderNavigationButtonKey] = [] + for (key, _) in self.leftButtonNodes { + if !leftButtons.contains(where: { $0.key == key }) { + removeKeys.append(key) + } + } + for key in removeKeys { + if let buttonNode = self.leftButtonNodes.removeValue(forKey: key) { + buttonNode.removeFromSupernode() + } + } + } else { + var nextRegularButtonOrigin = 16.0 + var nextExpandedButtonOrigin = 16.0 + for spec in leftButtons.reversed() { + if let buttonNode = self.leftButtonNodes[spec.key] { + let buttonSize = buttonNode.bounds.size + var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin + let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) + nextButtonOrigin += buttonSize.width + 4.0 + if spec.isForExpandedView { + nextExpandedButtonOrigin = nextButtonOrigin + } else { + nextRegularButtonOrigin = nextButtonOrigin + } + transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) + let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) + + var buttonTransition = transition + if case let .animated(duration, curve) = buttonTransition, alphaFactor == 0.0 { + buttonTransition = .animated(duration: duration * 0.25, curve: curve) + } + buttonTransition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) + } + } + } + + if self.currentRightButtons != rightButtons || presentationData.strings !== self.presentationData?.strings { + self.currentRightButtons = rightButtons + + var nextRegularButtonOrigin = size.width - 16.0 + var nextExpandedButtonOrigin = size.width - 16.0 + for spec in rightButtons.reversed() { + let buttonNode: PeerInfoHeaderNavigationButton + var wasAdded = false + + var key = spec.key + if key == .more || key == .search { + key = .moreToSearch + } + + if let current = self.rightButtonNodes[key] { + buttonNode = current + } else { + wasAdded = true + buttonNode = PeerInfoHeaderNavigationButton() + self.rightButtonNodes[key] = buttonNode + self.addSubnode(buttonNode) + } + buttonNode.action = { [weak self] _, gesture in + guard let strongSelf = self, let buttonNode = strongSelf.rightButtonNodes[key] else { + return + } + strongSelf.performAction?(spec.key, buttonNode.contextSourceNode, gesture) + } + let buttonSize = buttonNode.update(key: spec.key, presentationData: presentationData, height: size.height) + var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin + var buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin - buttonSize.width, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) + if case .postStory = spec.key { + buttonFrame.origin.x -= 12.0 + } + nextButtonOrigin -= buttonSize.width + 4.0 + if spec.isForExpandedView { + nextExpandedButtonOrigin = nextButtonOrigin + } else { + nextRegularButtonOrigin = nextButtonOrigin + } + let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) + if wasAdded { + buttonNode.isWhite = self.isWhite + + if key == .moreToSearch { + buttonNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) + } + + buttonNode.frame = buttonFrame + buttonNode.alpha = 0.0 + transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) + } else { + transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) + transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) + } + } + var removeKeys: [PeerInfoHeaderNavigationButtonKey] = [] + for (key, _) in self.rightButtonNodes { + if key == .moreToSearch { + if !rightButtons.contains(where: { $0.key == .more || $0.key == .search }) { + removeKeys.append(key) + } + } else if !rightButtons.contains(where: { $0.key == key }) { + removeKeys.append(key) + } + } + for key in removeKeys { + if let buttonNode = self.rightButtonNodes.removeValue(forKey: key) { + if key == .moreToSearch { + buttonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak buttonNode] _ in + buttonNode?.removeFromSupernode() + }) + buttonNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) + } else { + buttonNode.removeFromSupernode() + } + } + } + } else { + var nextRegularButtonOrigin = size.width - 16.0 + var nextExpandedButtonOrigin = size.width - 16.0 + + for spec in rightButtons.reversed() { + var key = spec.key + if key == .more || key == .search { + key = .moreToSearch + } + + if let buttonNode = self.rightButtonNodes[key] { + let buttonSize = buttonNode.bounds.size + var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin + var buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin - buttonSize.width, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) + if case .postStory = spec.key { + buttonFrame.origin.x -= 12.0 + } + nextButtonOrigin -= buttonSize.width + 4.0 + if spec.isForExpandedView { + nextExpandedButtonOrigin = nextButtonOrigin + } else { + nextRegularButtonOrigin = nextButtonOrigin + } + transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) + let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) + + var buttonTransition = transition + if case let .animated(duration, curve) = buttonTransition, alphaFactor == 0.0 { + buttonTransition = .animated(duration: duration * 0.25, curve: curve) + } + buttonTransition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) + } + } + } + self.presentationData = presentationData + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 7171e6041b..3eed39518e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -35,345 +35,7 @@ import AvatarStoryIndicatorComponent import ComponentDisplayAdapters import ChatAvatarNavigationNode import MultiScaleTextNode - -enum PeerInfoHeaderButtonKey: Hashable { - case message - case discussion - case call - case videoCall - case voiceChat - case mute - case more - case addMember - case search - case leave - case stop - case addContact -} - -enum PeerInfoHeaderButtonIcon { - case message - case call - case videoCall - case voiceChat - case mute - case unmute - case more - case addMember - case search - case leave - case stop -} - -final class PeerInfoHeaderButtonNode: HighlightableButtonNode { - let key: PeerInfoHeaderButtonKey - private let action: (PeerInfoHeaderButtonNode, ContextGesture?) -> Void - let referenceNode: ContextReferenceContentNode - let containerNode: ContextControllerSourceNode - private let backgroundNode: ASDisplayNode - private let iconNode: ASImageNode - private let textNode: ImmediateTextNode - private var animationNode: AnimationNode? - - private var theme: PresentationTheme? - private var icon: PeerInfoHeaderButtonIcon? - private var isActive: Bool? - - init(key: PeerInfoHeaderButtonKey, action: @escaping (PeerInfoHeaderButtonNode, ContextGesture?) -> Void) { - self.key = key - self.action = action - - self.referenceNode = ContextReferenceContentNode() - self.containerNode = ContextControllerSourceNode() - self.containerNode.animateScale = false - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.cornerRadius = 11.0 - - self.iconNode = ASImageNode() - self.iconNode.displaysAsynchronously = false - self.iconNode.displayWithoutProcessing = true - self.iconNode.isUserInteractionEnabled = false - - self.textNode = ImmediateTextNode() - self.textNode.displaysAsynchronously = false - self.textNode.isUserInteractionEnabled = false - - super.init() - - self.accessibilityTraits = .button - - self.containerNode.addSubnode(self.referenceNode) - self.referenceNode.addSubnode(self.backgroundNode) - self.referenceNode.addSubnode(self.iconNode) - self.addSubnode(self.containerNode) - self.addSubnode(self.textNode) - - self.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.layer.removeAnimation(forKey: "opacity") - strongSelf.alpha = 0.4 - } else { - strongSelf.alpha = 1.0 - strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - } - - self.containerNode.activated = { [weak self] gesture, _ in - if let strongSelf = self { - strongSelf.action(strongSelf, gesture) - } - } - - self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - } - - @objc private func buttonPressed() { - switch self.icon { - case .voiceChat, .more, .leave: - self.animationNode?.playOnce() - default: - break - } - self.action(self, nil) - } - - func update(size: CGSize, text: String, icon: PeerInfoHeaderButtonIcon, isActive: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { - let previousIcon = self.icon - let themeUpdated = self.theme != presentationData.theme - let iconUpdated = self.icon != icon - let isActiveUpdated = self.isActive != isActive - self.isActive = isActive - - let iconSize = CGSize(width: 40.0, height: 40.0) - - if themeUpdated || iconUpdated { - self.theme = presentationData.theme - self.icon = icon - - var isGestureEnabled = false - if [.mute, .voiceChat, .more].contains(icon) { - isGestureEnabled = true - } - self.containerNode.isGestureEnabled = isGestureEnabled - - let animationName: String? - var colors: [String: UIColor] = [:] - var playOnce = false - var seekToEnd = false - let iconColor = presentationData.theme.list.itemAccentColor - switch icon { - case .voiceChat: - animationName = "anim_profilevc" - colors = ["Line 3.Group 1.Stroke 1": iconColor, - "Line 1.Group 1.Stroke 1": iconColor, - "Line 2.Group 1.Stroke 1": iconColor] - case .mute: - animationName = "anim_profileunmute" - colors = ["Middle.Group 1.Fill 1": iconColor, - "Top.Group 1.Fill 1": iconColor, - "Bottom.Group 1.Fill 1": iconColor, - "EXAMPLE.Group 1.Fill 1": iconColor, - "Line.Group 1.Stroke 1": iconColor] - if previousIcon == .unmute { - playOnce = true - } else { - seekToEnd = true - } - case .unmute: - animationName = "anim_profilemute" - colors = ["Middle.Group 1.Fill 1": iconColor, - "Top.Group 1.Fill 1": iconColor, - "Bottom.Group 1.Fill 1": iconColor, - "EXAMPLE.Group 1.Fill 1": iconColor, - "Line.Group 1.Stroke 1": iconColor] - if previousIcon == .mute { - playOnce = true - } else { - seekToEnd = true - } - case .more: - animationName = "anim_profilemore" - colors = ["Point 2.Group 1.Fill 1": iconColor, - "Point 3.Group 1.Fill 1": iconColor, - "Point 1.Group 1.Fill 1": iconColor] - case .leave: - animationName = "anim_profileleave" - colors = ["Arrow.Group 2.Stroke 1": iconColor, - "Door.Group 1.Stroke 1": iconColor, - "Arrow.Group 1.Stroke 1": iconColor] - default: - animationName = nil - } - - if let animationName = animationName { - let animationNode: AnimationNode - if let current = self.animationNode { - animationNode = current - animationNode.setAnimation(name: animationName, colors: colors) - } else { - animationNode = AnimationNode(animation: animationName, colors: colors, scale: 1.0) - self.referenceNode.addSubnode(animationNode) - self.animationNode = animationNode - } - } else if let animationNode = self.animationNode { - self.animationNode = nil - animationNode.removeFromSupernode() - } - - if playOnce { - self.animationNode?.play() - } else if seekToEnd { - self.animationNode?.seekToEnd() - } - - self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) - self.iconNode.image = generateImage(iconSize, contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setBlendMode(.normal) - context.setFillColor(iconColor.cgColor) - let imageName: String? - switch icon { - case .message: - imageName = "Peer Info/ButtonMessage" - case .call: - imageName = "Peer Info/ButtonCall" - case .videoCall: - imageName = "Peer Info/ButtonVideo" - case .voiceChat: - imageName = nil - case .mute: - imageName = nil - case .unmute: - imageName = nil - case .more: - imageName = nil - case .addMember: - imageName = "Peer Info/ButtonAddMember" - case .search: - imageName = "Peer Info/ButtonSearch" - case .leave: - imageName = nil - case .stop: - imageName = "Peer Info/ButtonStop" - } - if let imageName = imageName, let image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: .white) { - let imageRect = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) - context.clip(to: imageRect, mask: image.cgImage!) - context.fill(imageRect) - } - }) - } - - if isActiveUpdated { - let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) - alphaTransition.updateAlpha(node: self.iconNode, alpha: isActive ? 1.0 : 0.3) - if let animationNode = self.animationNode { - alphaTransition.updateAlpha(node: animationNode, alpha: isActive ? 1.0 : 0.3) - } - alphaTransition.updateAlpha(node: self.textNode, alpha: isActive ? 1.0 : 0.3) - } - - self.textNode.attributedText = NSAttributedString(string: text.lowercased(), font: Font.regular(11.0), textColor: presentationData.theme.list.itemAccentColor) - self.accessibilityLabel = text - let titleSize = self.textNode.updateLayout(CGSize(width: 120.0, height: .greatestFiniteMagnitude)) - - transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size)) - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) - transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: 1.0), size: iconSize)) - if let animationNode = self.animationNode { - transition.updateFrame(node: animationNode, frame: CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: 1.0), size: iconSize)) - } - transition.updateFrameAdditiveToCenter(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: size.height - titleSize.height - 9.0), size: titleSize)) - - self.referenceNode.frame = self.containerNode.bounds - } -} - -final class PeerInfoHeaderActionButtonNode: HighlightableButtonNode { - let key: PeerInfoHeaderButtonKey - private let action: (PeerInfoHeaderActionButtonNode, ContextGesture?) -> Void - let referenceNode: ContextReferenceContentNode - let containerNode: ContextControllerSourceNode - private let backgroundNode: ASDisplayNode - private let textNode: ImmediateTextNode - - private var theme: PresentationTheme? - - init(key: PeerInfoHeaderButtonKey, action: @escaping (PeerInfoHeaderActionButtonNode, ContextGesture?) -> Void) { - self.key = key - self.action = action - - self.referenceNode = ContextReferenceContentNode() - self.containerNode = ContextControllerSourceNode() - self.containerNode.animateScale = false - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.cornerRadius = 11.0 - - self.textNode = ImmediateTextNode() - self.textNode.displaysAsynchronously = false - self.textNode.isUserInteractionEnabled = false - - super.init() - - self.accessibilityTraits = .button - - self.containerNode.addSubnode(self.referenceNode) - self.referenceNode.addSubnode(self.backgroundNode) - self.addSubnode(self.containerNode) - self.addSubnode(self.textNode) - - self.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.layer.removeAnimation(forKey: "opacity") - strongSelf.alpha = 0.4 - } else { - strongSelf.alpha = 1.0 - strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - } - - self.containerNode.activated = { [weak self] gesture, _ in - if let strongSelf = self { - strongSelf.action(strongSelf, gesture) - } - } - - self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - } - - @objc private func buttonPressed() { - self.action(self, nil) - } - - func update(size: CGSize, text: String, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { - let themeUpdated = self.theme != presentationData.theme - if themeUpdated { - self.theme = presentationData.theme - - self.containerNode.isGestureEnabled = false - - self.backgroundNode.backgroundColor = presentationData.theme.list.itemAccentColor - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) - } - - self.textNode.attributedText = NSAttributedString(string: text, font: Font.semibold(16.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor) - self.accessibilityLabel = text - let titleSize = self.textNode.updateLayout(CGSize(width: 120.0, height: .greatestFiniteMagnitude)) - - transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size)) - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) - transition.updateFrameAdditiveToCenter(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)) - - self.referenceNode.frame = self.containerNode.bounds - } -} +import PeerInfoCoverComponent final class PeerInfoHeaderNavigationTransition { let sourceNavigationBar: NavigationBar @@ -393,1445 +55,7 @@ final class PeerInfoHeaderNavigationTransition { } } -final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { - let context: AccountContext - - let containerNode: ContextControllerSourceNode - - let avatarNode: AvatarNode - private(set) var avatarStoryView: ComponentView? - fileprivate var videoNode: UniversalVideoNode? - fileprivate var markupNode: AvatarVideoNode? - fileprivate var iconView: ComponentView? - private var videoContent: NativeVideoContent? - private var videoStartTimestamp: Double? - - var isExpanded: Bool = false - var canAttachVideo: Bool = true { - didSet { - if oldValue != self.canAttachVideo { - self.videoNode?.canAttachContent = !self.isExpanded && self.canAttachVideo - } - } - } - - var tapped: (() -> Void)? - var emojiTapped: (() -> Void)? - var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? - - private var isFirstAvatarLoading = true - var item: PeerInfoAvatarListItem? - - private let playbackStartDisposable = MetaDisposable() - - var storyData: (totalCount: Int, unseenCount: Int, hasUnseenCloseFriends: Bool)? - var storyProgress: Float? - - init(context: AccountContext) { - self.context = context - self.containerNode = ContextControllerSourceNode() - - let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0)) - self.avatarNode = AvatarNode(font: avatarFont) - - super.init() - - self.addSubnode(self.containerNode) - self.containerNode.addSubnode(self.avatarNode) - self.containerNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0)) - self.avatarNode.frame = self.containerNode.bounds - - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) - self.avatarNode.view.addGestureRecognizer(tapGestureRecognizer) - - self.containerNode.activated = { [weak self] gesture, _ in - guard let strongSelf = self else { - return - } - tapGestureRecognizer.isEnabled = false - tapGestureRecognizer.isEnabled = true - strongSelf.contextAction?(strongSelf.containerNode, gesture) - } - } - - deinit { - self.playbackStartDisposable.dispose() - } - - func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme) { - var colors = AvatarNode.Colors(theme: theme) - colors.seenColors = [ - theme.list.controlSecondaryColor, - theme.list.controlSecondaryColor - ] - var storyStats: AvatarNode.StoryStats? - if let storyData = self.storyData { - storyStats = AvatarNode.StoryStats( - totalCount: storyData.totalCount, - unseenCount: storyData.unseenCount, - hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends, - progress: self.storyProgress - ) - } else if let storyProgress = self.storyProgress { - storyStats = AvatarNode.StoryStats( - totalCount: 1, - unseenCount: 1, - hasUnseenCloseFriendsItems: false, - progress: storyProgress - ) - } - self.avatarNode.setStoryStats(storyStats: storyStats, presentationParams: AvatarNode.StoryPresentationParams( - colors: colors, - lineWidth: 3.0, - inactiveLineWidth: 1.5 - ), transition: Transition(transition)) - } - - @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.tapped?() - } - } - - @objc private func emojiTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.emojiTapped?() - } - } - - func updateTransitionFraction(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) { - if let videoNode = self.videoNode { - if case .immediate = transition, fraction == 1.0 { - return - } - if fraction > 0.0 { - videoNode.pause() - } else { - videoNode.play() - } - transition.updateAlpha(node: videoNode, alpha: 1.0 - fraction) - } - if let markupNode = self.markupNode { - if case .immediate = transition, fraction == 1.0 { - return - } - if fraction > 0.0 { - markupNode.updateVisibility(false) - } else { - markupNode.updateVisibility(true) - } - transition.updateAlpha(node: markupNode, alpha: 1.0 - fraction) - } - } - - var removedPhotoResourceIds = Set() - func update(peer: Peer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) { - if let peer = peer { - let previousItem = self.item - var item = item - self.item = item - - var overrideImage: AvatarNodeImageOverride? - if peer.isDeleted { - overrideImage = .deletedIcon - } else if let previousItem = previousItem, item == nil { - if case let .image(_, representations, _, _, _, _) = previousItem, let rep = representations.last { - self.removedPhotoResourceIds.insert(rep.representation.resource.id.stringRepresentation) - } - overrideImage = AvatarNodeImageOverride.none - item = nil - } else if let rep = peer.profileImageRepresentations.last, self.removedPhotoResourceIds.contains(rep.resource.id.stringRepresentation) { - overrideImage = AvatarNodeImageOverride.none - item = nil - } - - if let _ = overrideImage { - self.containerNode.isGestureEnabled = false - } else if peer.profileImageRepresentations.isEmpty { - self.containerNode.isGestureEnabled = false - } else { - self.containerNode.isGestureEnabled = false - } - - self.avatarNode.imageNode.animateFirstTransition = !isSettings - self.avatarNode.setPeer(context: self.context, theme: theme, peer: EnginePeer(peer), overrideImage: overrideImage, clipStyle: .none, synchronousLoad: self.isFirstAvatarLoading, displayDimensions: CGSize(width: avatarSize, height: avatarSize), storeUnrounded: true) - - if let threadInfo = threadInfo { - self.avatarNode.isHidden = true - - let iconView: ComponentView - if let current = self.iconView { - iconView = current - } else { - iconView = ComponentView() - self.iconView = iconView - } - let content: EmojiStatusComponent.Content - if threadId == 1 { - content = .image(image: PresentationResourcesChat.chatGeneralThreadIcon(theme)) - } else if let iconFileId = threadInfo.icon { - content = .animation(content: .customEmoji(fileId: iconFileId), size: CGSize(width: avatarSize, height: avatarSize), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .forever) - } else { - content = .topic(title: String(threadInfo.title.prefix(1)), color: threadInfo.iconColor, size: CGSize(width: avatarSize, height: avatarSize)) - } - let _ = iconView.update( - transition: .immediate, - component: AnyComponent(EmojiStatusComponent( - context: self.context, - animationCache: self.context.animationCache, - animationRenderer: self.context.animationRenderer, - content: content, - isVisibleForAnimations: true, - action: nil - )), - environment: {}, - containerSize: CGSize(width: avatarSize, height: avatarSize) - ) - if let iconComponentView = iconView.view { - iconComponentView.isUserInteractionEnabled = true - if iconComponentView.superview == nil { - iconComponentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.emojiTapGesture(_:)))) - self.avatarNode.view.superview?.addSubview(iconComponentView) - } - iconComponentView.frame = CGRect(origin: CGPoint(), size: CGSize(width: avatarSize, height: avatarSize)) - } - } - - var isForum = false - let avatarCornerRadius: CGFloat - if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { - avatarCornerRadius = floor(avatarSize * 0.25) - isForum = true - } else { - avatarCornerRadius = avatarSize / 2.0 - } - if self.avatarNode.layer.cornerRadius != 0.0 { - ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut).updateCornerRadius(layer: self.avatarNode.contentNode.layer, cornerRadius: avatarCornerRadius) - } else { - self.avatarNode.contentNode.layer.cornerRadius = avatarCornerRadius - } - self.avatarNode.contentNode.layer.masksToBounds = true - - self.isFirstAvatarLoading = false - - self.containerNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize)) - self.avatarNode.frame = self.containerNode.bounds - self.avatarNode.font = avatarPlaceholderFont(size: floor(avatarSize * 16.0 / 37.0)) - - if let item = item { - let representations: [ImageRepresentationWithReference] - let videoRepresentations: [VideoRepresentationWithReference] - let immediateThumbnailData: Data? - var videoId: Int64 - let markup: TelegramMediaImage.EmojiMarkup? - switch item { - case .custom: - representations = [] - videoRepresentations = [] - immediateThumbnailData = nil - videoId = 0 - markup = nil - case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): - representations = topRepresentations - videoRepresentations = videoRepresentationsValue - immediateThumbnailData = immediateThumbnail - videoId = peer.id.id._internalGetInt64Value() - if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { - videoId = videoId &+ resource.photoId - } - markup = nil - case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, markupValue): - representations = imageRepresentations - videoRepresentations = videoRepresentationsValue - immediateThumbnailData = immediateThumbnail - if case let .cloud(imageId, _, _) = reference { - videoId = imageId - } else { - videoId = peer.id.id._internalGetInt64Value() - } - markup = markupValue - } - - self.containerNode.isGestureEnabled = !isSettings - - if let markup { - if let videoNode = self.videoNode { - self.videoContent = nil - self.videoStartTimestamp = nil - self.videoNode = nil - - videoNode.removeFromSupernode() - } - - let markupNode: AvatarVideoNode - if let current = self.markupNode { - markupNode = current - } else { - markupNode = AvatarVideoNode(context: self.context) - self.avatarNode.contentNode.addSubnode(markupNode) - self.markupNode = markupNode - } - markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0)) - markupNode.updateVisibility(true) - } else if threadInfo == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) { - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil)])) - let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil) - if videoContent.id != self.videoContent?.id { - self.videoNode?.removeFromSupernode() - - let mediaManager = self.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .embedded) - videoNode.isUserInteractionEnabled = false - videoNode.isHidden = true - - if let startTimestamp = video.representation.startTimestamp { - self.videoStartTimestamp = startTimestamp - self.playbackStartDisposable.set((videoNode.status - |> map { status -> Bool in - if let status = status, case .playing = status.status { - return true - } else { - return false - } - } - |> filter { playing in - return playing - } - |> take(1) - |> deliverOnMainQueue).start(completed: { [weak self] in - if let strongSelf = self { - Queue.mainQueue().after(0.15) { - strongSelf.videoNode?.isHidden = false - } - } - })) - } else { - self.videoStartTimestamp = nil - self.playbackStartDisposable.set(nil) - videoNode.isHidden = false - } - - self.videoContent = videoContent - self.videoNode = videoNode - - let maskPath: UIBezierPath - if isForum { - maskPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size), cornerRadius: avatarCornerRadius) - } else { - maskPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size)) - } - let shape = CAShapeLayer() - shape.path = maskPath.cgPath - videoNode.layer.mask = shape - - self.avatarNode.contentNode.addSubnode(videoNode) - } - } else { - if let markupNode = self.markupNode { - self.markupNode = nil - markupNode.removeFromSupernode() - } - if let videoNode = self.videoNode { - self.videoStartTimestamp = nil - self.videoContent = nil - self.videoNode = nil - - videoNode.removeFromSupernode() - } - } - } else { - if let markupNode = self.markupNode { - self.markupNode = nil - markupNode.removeFromSupernode() - } - if let videoNode = self.videoNode { - self.videoStartTimestamp = nil - self.videoContent = nil - self.videoNode = nil - - videoNode.removeFromSupernode() - } - self.containerNode.isGestureEnabled = false - } - - if let markupNode = self.markupNode { - markupNode.frame = self.avatarNode.bounds - markupNode.updateLayout(size: self.avatarNode.bounds.size, cornerRadius: avatarCornerRadius, transition: .immediate) - } - - if let videoNode = self.videoNode { - if self.canAttachVideo { - videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate) - } - videoNode.frame = self.avatarNode.contentNode.bounds - - if isExpanded == videoNode.canAttachContent { - self.isExpanded = isExpanded - let update = { - videoNode.canAttachContent = !self.isExpanded && self.canAttachVideo - if videoNode.canAttachContent { - videoNode.play() - } - } - if isExpanded { - DispatchQueue.main.async { - update() - } - } else { - update() - } - } - } - } - - self.updateStoryView(transition: .immediate, theme: theme) - } -} - -final class PeerInfoEditingAvatarOverlayNode: ASDisplayNode { - private let context: AccountContext - - private let imageNode: ImageNode - private let updatingAvatarOverlay: ASImageNode - private let iconNode: ASImageNode - private var statusNode: RadialStatusNode - - private var currentRepresentation: TelegramMediaImageRepresentation? - - init(context: AccountContext) { - self.context = context - - self.imageNode = ImageNode(enableEmpty: true) - - self.updatingAvatarOverlay = ASImageNode() - self.updatingAvatarOverlay.displayWithoutProcessing = true - self.updatingAvatarOverlay.displaysAsynchronously = false - self.updatingAvatarOverlay.alpha = 0.0 - - self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.6)) - self.statusNode.isUserInteractionEnabled = false - - self.iconNode = ASImageNode() - self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIconLarge"), color: .white) - self.iconNode.alpha = 0.0 - - super.init() - - self.imageNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0)) - self.updatingAvatarOverlay.frame = self.imageNode.frame - - let radialStatusSize: CGFloat = 50.0 - let imagePosition = self.imageNode.position - self.statusNode.frame = CGRect(origin: CGPoint(x: floor(imagePosition.x - radialStatusSize / 2.0), y: floor(imagePosition.y - radialStatusSize / 2.0)), size: CGSize(width: radialStatusSize, height: radialStatusSize)) - - if let image = self.iconNode.image { - self.iconNode.frame = CGRect(origin: CGPoint(x: floor(imagePosition.x - image.size.width / 2.0), y: floor(imagePosition.y - image.size.height / 2.0)), size: image.size) - } - - self.addSubnode(self.imageNode) - self.addSubnode(self.updatingAvatarOverlay) - self.addSubnode(self.statusNode) - } - - func updateTransitionFraction(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) { - transition.updateAlpha(node: self, alpha: 1.0 - fraction) - } - - func update(peer: Peer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, item: PeerInfoAvatarListItem?, updatingAvatar: PeerInfoUpdatingAvatar?, uploadProgress: AvatarUploadProgress?, theme: PresentationTheme, avatarSize: CGFloat, isEditing: Bool) { - guard let peer = peer else { - return - } - - self.imageNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize)) - self.updatingAvatarOverlay.frame = self.imageNode.frame - - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear) - - let clipStyle: AvatarNodeClipStyle - if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { - clipStyle = .roundedRect - } else { - clipStyle = .round - } - - var isPersonal = false - if let updatingAvatar, case let .image(image) = updatingAvatar, image.isPersonal { - isPersonal = true - } - - if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) - || isPersonal - || self.currentRepresentation != nil && updatingAvatar == nil { - var overlayHidden = true - if let updatingAvatar = updatingAvatar { - overlayHidden = false - - var cancelEnabled = true - let progressValue: CGFloat? - if let uploadProgress { - switch uploadProgress { - case let .value(value): - progressValue = max(0.027, value) - case .indefinite: - progressValue = nil - cancelEnabled = false - } - } else { - progressValue = 0.027 - } - self.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: progressValue, cancelEnabled: cancelEnabled, animateRotation: true)) - - if case let .image(representation) = updatingAvatar { - if representation != self.currentRepresentation { - self.currentRepresentation = representation - - if let signal = peerAvatarImage(account: context.account, peerReference: nil, authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: avatarSize, height: avatarSize), clipStyle: clipStyle, emptyColor: nil, synchronousLoad: false, provideUnrounded: false) { - self.imageNode.setSignal(signal |> map { $0?.0 }) - } - } - } - - transition.updateAlpha(node: self.updatingAvatarOverlay, alpha: 1.0) - } else { - let targetOverlayAlpha: CGFloat = 0.0 - if self.updatingAvatarOverlay.alpha != targetOverlayAlpha { - let update = { - self.statusNode.transitionToState(.none) - self.currentRepresentation = nil - self.imageNode.setSignal(.single(nil)) - transition.updateAlpha(node: self.updatingAvatarOverlay, alpha: overlayHidden ? 0.0 : 1.0) - } - Queue.mainQueue().after(0.3) { - update() - } - } - } - if !overlayHidden && self.updatingAvatarOverlay.image == nil { - switch clipStyle { - case .round: - self.updatingAvatarOverlay.image = generateFilledCircleImage(diameter: avatarSize, color: UIColor(white: 0.0, alpha: 0.4), backgroundColor: nil) - case .roundedRect: - self.updatingAvatarOverlay.image = generateFilledRoundedRectImage(size: CGSize(width: avatarSize, height: avatarSize), cornerRadius: avatarSize * 0.25, color: UIColor(white: 0.0, alpha: 0.4), backgroundColor: nil) - default: - break - } - } - } else { - self.statusNode.transitionToState(.none) - self.currentRepresentation = nil - transition.updateAlpha(node: self.iconNode, alpha: 0.0) - transition.updateAlpha(node: self.updatingAvatarOverlay, alpha: 0.0) - } - } -} - -final class PeerInfoEditingAvatarNode: ASDisplayNode { - private let context: AccountContext - let avatarNode: AvatarNode - fileprivate var videoNode: UniversalVideoNode? - fileprivate var markupNode: AvatarVideoNode? - private var videoContent: NativeVideoContent? - private var videoStartTimestamp: Double? - var item: PeerInfoAvatarListItem? - - var tapped: ((Bool) -> Void)? - - var canAttachVideo: Bool = true - - init(context: AccountContext) { - self.context = context - let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0)) - self.avatarNode = AvatarNode(font: avatarFont) - - super.init() - - self.addSubnode(self.avatarNode) - self.avatarNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0)) - - self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.tapped?(false) - } - } - - func reset() { - guard let videoNode = self.videoNode else { - return - } - videoNode.isHidden = true - videoNode.seek(self.videoStartTimestamp ?? 0.0) - Queue.mainQueue().after(0.15) { - videoNode.isHidden = false - } - } - - var removedPhotoResourceIds = Set() - func update(peer: Peer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, item: PeerInfoAvatarListItem?, updatingAvatar: PeerInfoUpdatingAvatar?, uploadProgress: AvatarUploadProgress?, theme: PresentationTheme, avatarSize: CGFloat, isEditing: Bool) { - guard let peer = peer else { - return - } - - let canEdit = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) - - let previousItem = self.item - var item = item - self.item = item - - let overrideImage: AvatarNodeImageOverride? - if canEdit, peer.profileImageRepresentations.isEmpty { - overrideImage = .editAvatarIcon(forceNone: true) - } else if let previousItem = previousItem, item == nil { - if case let .image(_, representations, _, _, _, _) = previousItem, let rep = representations.last { - self.removedPhotoResourceIds.insert(rep.representation.resource.id.stringRepresentation) - } - overrideImage = canEdit ? .editAvatarIcon(forceNone: true) : AvatarNodeImageOverride.none - item = nil - } else if let representation = peer.profileImageRepresentations.last, self.removedPhotoResourceIds.contains(representation.resource.id.stringRepresentation) { - overrideImage = canEdit ? .editAvatarIcon(forceNone: true) : AvatarNodeImageOverride.none - item = nil - } else { - overrideImage = item == nil && canEdit ? .editAvatarIcon(forceNone: true) : nil - } - self.avatarNode.font = avatarPlaceholderFont(size: floor(avatarSize * 16.0 / 37.0)) - self.avatarNode.setPeer(context: self.context, theme: theme, peer: EnginePeer(peer), overrideImage: overrideImage, clipStyle: .none, synchronousLoad: false, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) - self.avatarNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize)) - - var isForum = false - let avatarCornerRadius: CGFloat - if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { - isForum = true - avatarCornerRadius = floor(avatarSize * 0.25) - } else { - avatarCornerRadius = avatarSize / 2.0 - } - if self.avatarNode.layer.cornerRadius != 0.0 { - ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut).updateCornerRadius(layer: self.avatarNode.layer, cornerRadius: avatarCornerRadius) - } else { - self.avatarNode.layer.cornerRadius = avatarCornerRadius - } - self.avatarNode.layer.masksToBounds = true - - if let item = item { - let representations: [ImageRepresentationWithReference] - let videoRepresentations: [VideoRepresentationWithReference] - let immediateThumbnailData: Data? - var videoId: Int64 - let markup: TelegramMediaImage.EmojiMarkup? - switch item { - case .custom: - representations = [] - videoRepresentations = [] - immediateThumbnailData = nil - videoId = 0 - markup = nil - case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): - representations = topRepresentations - videoRepresentations = videoRepresentationsValue - immediateThumbnailData = immediateThumbnail - videoId = peer.id.id._internalGetInt64Value() - if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { - videoId = videoId &+ resource.photoId - } - markup = nil - case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, markupValue): - representations = imageRepresentations - videoRepresentations = videoRepresentationsValue - immediateThumbnailData = immediateThumbnail - if case let .cloud(imageId, _, _) = reference { - videoId = imageId - } else { - videoId = peer.id.id._internalGetInt64Value() - } - markup = markupValue - } - - if let markup { - if let videoNode = self.videoNode { - self.videoContent = nil - self.videoStartTimestamp = nil - self.videoNode = nil - - videoNode.removeFromSupernode() - } - - let markupNode: AvatarVideoNode - if let current = self.markupNode { - markupNode = current - } else { - markupNode = AvatarVideoNode(context: self.context) - self.avatarNode.contentNode.addSubnode(markupNode) - self.markupNode = markupNode - } - markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0)) - markupNode.updateVisibility(true) - } else if threadData == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) { - if let markupNode = self.markupNode { - self.markupNode = nil - markupNode.removeFromSupernode() - } - - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil)])) - let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil) - if videoContent.id != self.videoContent?.id { - self.videoNode?.removeFromSupernode() - - let mediaManager = self.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .gallery) - videoNode.isUserInteractionEnabled = false - self.videoStartTimestamp = video.representation.startTimestamp - self.videoContent = videoContent - self.videoNode = videoNode - - let maskPath: UIBezierPath - if isForum { - maskPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size), cornerRadius: avatarCornerRadius) - } else { - maskPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size)) - } - let shape = CAShapeLayer() - shape.path = maskPath.cgPath - videoNode.layer.mask = shape - - self.avatarNode.contentNode.addSubnode(videoNode) - } - } else { - if let markupNode = self.markupNode { - self.markupNode = nil - markupNode.removeFromSupernode() - } - if let videoNode = self.videoNode { - self.videoStartTimestamp = nil - self.videoContent = nil - self.videoNode = nil - - videoNode.removeFromSupernode() - } - } - } else if let videoNode = self.videoNode { - self.videoStartTimestamp = nil - self.videoContent = nil - self.videoNode = nil - - videoNode.removeFromSupernode() - } - - if let markupNode = self.markupNode { - markupNode.frame = self.avatarNode.bounds - markupNode.updateLayout(size: self.avatarNode.bounds.size, cornerRadius: avatarCornerRadius, transition: .immediate) - } - - if let videoNode = self.videoNode { - if self.canAttachVideo { - videoNode.updateLayout(size: self.avatarNode.bounds.size, transition: .immediate) - } - videoNode.frame = self.avatarNode.bounds - - if isEditing != videoNode.canAttachContent { - videoNode.canAttachContent = isEditing && self.canAttachVideo - } - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.avatarNode.frame.contains(point) { - return self.avatarNode.view - } - return super.hitTest(point, with: event) - } -} - - - -final class PeerInfoAvatarListNode: ASDisplayNode { - private let isSettings: Bool - let containerNode: ASDisplayNode - let pinchSourceNode: PinchSourceContainerNode - let bottomCoverNode: ASDisplayNode - fileprivate let maskNode: DynamicIslandMaskNode - fileprivate let topCoverNode: DynamicIslandBlurNode - let avatarContainerNode: PeerInfoAvatarTransformContainerNode - let listContainerTransformNode: ASDisplayNode - let listContainerNode: PeerInfoAvatarListContainerNode - - let isReady = Promise() - - var arguments: (Peer?, Int64?, EngineMessageHistoryThread.Info?, PresentationTheme, CGFloat, Bool)? - var item: PeerInfoAvatarListItem? - - var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)? - var animateOverlaysFadeIn: (() -> Void)? - var openStories: (() -> Void)? - - init(context: AccountContext, readyWhenGalleryLoads: Bool, isSettings: Bool) { - self.isSettings = isSettings - - self.containerNode = ASDisplayNode() - - self.bottomCoverNode = ASDisplayNode() - - self.maskNode = DynamicIslandMaskNode() - self.pinchSourceNode = PinchSourceContainerNode() - - self.avatarContainerNode = PeerInfoAvatarTransformContainerNode(context: context) - self.listContainerTransformNode = ASDisplayNode() - self.listContainerNode = PeerInfoAvatarListContainerNode(context: context, isSettings: isSettings) - self.listContainerNode.clipsToBounds = true - self.listContainerNode.isHidden = true - - self.topCoverNode = DynamicIslandBlurNode() - - super.init() - - self.addSubnode(self.containerNode) - self.containerNode.addSubnode(self.bottomCoverNode) - self.containerNode.addSubnode(self.pinchSourceNode) - self.pinchSourceNode.contentNode.addSubnode(self.avatarContainerNode) - self.listContainerTransformNode.addSubnode(self.listContainerNode) - self.pinchSourceNode.contentNode.addSubnode(self.listContainerTransformNode) - self.containerNode.addSubnode(self.topCoverNode) - - let avatarReady = (self.avatarContainerNode.avatarNode.ready - |> mapToSignal { _ -> Signal in - return .complete() - } - |> then(.single(true))) - - let galleryReady = self.listContainerNode.isReady.get() - |> filter { value in - return value - } - |> take(1) - - let combinedSignal: Signal - if readyWhenGalleryLoads { - combinedSignal = combineLatest(queue: .mainQueue(), - avatarReady, - galleryReady - ) - |> map { lhs, rhs in - return lhs && rhs - } - } else { - combinedSignal = avatarReady - } - - self.isReady.set(combinedSignal - |> filter { value in - return value - } - |> take(1)) - - self.listContainerNode.itemsUpdated = { [weak self] items in - if let strongSelf = self { - strongSelf.item = items.first - strongSelf.itemsUpdated?(items) - if let (peer, threadId, threadInfo, theme, avatarSize, isExpanded) = strongSelf.arguments { - strongSelf.avatarContainerNode.update(peer: peer, threadId: threadId, threadInfo: threadInfo, item: strongSelf.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: strongSelf.isSettings) - } - } - } - - self.pinchSourceNode.activate = { [weak self] sourceNode in - guard let strongSelf = self, let (_, _, _, _, _, isExpanded) = strongSelf.arguments, isExpanded else { - return - } - let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { - return UIScreen.main.bounds - }) - context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) - - strongSelf.listContainerNode.bottomShadowNode.alpha = 0.0 - } - - self.pinchSourceNode.animatedOut = { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.animateOverlaysFadeIn?() - } - - self.listContainerNode.openStories = { [weak self] in - guard let self else { - return - } - self.openStories?() - } - } - - func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, isForum: Bool, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { - self.arguments = (peer, threadId, threadInfo, theme, avatarSize, isExpanded) - self.maskNode.isForum = isForum - self.pinchSourceNode.update(size: size, transition: transition) - self.containerNode.frame = CGRect(origin: CGPoint(), size: size) - self.pinchSourceNode.frame = CGRect(origin: CGPoint(), size: size) - self.avatarContainerNode.update(peer: peer, threadId: threadId, threadInfo: threadInfo, item: self.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: self.isSettings) - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.listContainerNode.isHidden { - if let result = self.listContainerNode.view.hitTest(self.view.convert(point, to: self.listContainerNode.view), with: event) { - return result - } - } else { - if let result = self.avatarContainerNode.avatarNode.view.hitTest(self.view.convert(point, to: self.avatarContainerNode.avatarNode.view), with: event) { - return result - } else if let result = self.avatarContainerNode.iconView?.view?.hitTest(self.view.convert(point, to: self.avatarContainerNode.iconView?.view), with: event) { - return result - } - } - - return super.hitTest(point, with: event) - } - - func animateAvatarCollapse(transition: ContainedViewLayoutTransition) { - if let currentItemNode = self.listContainerNode.currentItemNode, case .animated = transition { - if let _ = self.avatarContainerNode.videoNode { - - } else if let _ = self.avatarContainerNode.markupNode { - - } else if let unroundedImage = self.avatarContainerNode.avatarNode.unroundedImage { - let avatarCopyView = UIImageView() - avatarCopyView.image = unroundedImage - avatarCopyView.frame = self.avatarContainerNode.avatarNode.frame - avatarCopyView.center = currentItemNode.imageNode.position - currentItemNode.view.addSubview(avatarCopyView) - let scale = currentItemNode.imageNode.bounds.height / avatarCopyView.bounds.height - avatarCopyView.layer.transform = CATransform3DMakeScale(scale, scale, scale) - avatarCopyView.alpha = 0.0 - transition.updateAlpha(layer: avatarCopyView.layer, alpha: 1.0, completion: { [weak avatarCopyView] _ in - Queue.mainQueue().after(0.1, { - avatarCopyView?.removeFromSuperview() - }) - }) - } - } - } -} - -private enum MoreIconNodeState: Equatable { - case more - case search - case moreToSearch(Float) -} - -private final class MoreIconNode: ManagedAnimationNode { - private let duration: Double = 0.21 - private var iconState: MoreIconNodeState = .more - - init() { - super.init(size: CGSize(width: 30.0, height: 30.0)) - - self.trackTo(item: ManagedAnimationItem(source: .local("anim_moretosearch"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.0)) - } - - func play() { - if case .more = self.iconState { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_moredots"), frames: .range(startFrame: 0, endFrame: 46), duration: 0.76)) - } - } - - func enqueueState(_ state: MoreIconNodeState, animated: Bool) { - guard self.iconState != state else { - return - } - - let previousState = self.iconState - self.iconState = state - - let source = ManagedAnimationSource.local("anim_moretosearch") - - let totalLength: Int = 90 - if animated { - switch previousState { - case .more: - switch state { - case .more: - break - case .search: - self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: totalLength), duration: self.duration)) - case let .moreToSearch(progress): - let frame = Int(progress * Float(totalLength)) - let duration = self.duration * Double(progress) - self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: frame), duration: duration)) - } - case .search: - switch state { - case .more: - self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: 0), duration: self.duration)) - case .search: - break - case let .moreToSearch(progress): - let frame = Int(progress * Float(totalLength)) - let duration = self.duration * Double((1.0 - progress)) - self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: frame), duration: duration)) - } - case let .moreToSearch(currentProgress): - let currentFrame = Int(currentProgress * Float(totalLength)) - switch state { - case .more: - let duration = self.duration * Double(currentProgress) - self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: currentFrame, endFrame: 0), duration: duration)) - case .search: - let duration = self.duration * (1.0 - Double(currentProgress)) - self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: currentFrame, endFrame: totalLength), duration: duration)) - case let .moreToSearch(progress): - let frame = Int(progress * Float(totalLength)) - let duration = self.duration * Double(abs(currentProgress - progress)) - self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: currentFrame, endFrame: frame), duration: duration)) - } - } - } else { - switch state { - case .more: - self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: 0), duration: 0.0)) - case .search: - self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: totalLength), duration: 0.0)) - case let .moreToSearch(progress): - let frame = Int(progress * Float(totalLength)) - self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: frame, endFrame: frame), duration: 0.0)) - } - } - } -} - -final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { - let containerNode: ContextControllerSourceNode - let contextSourceNode: ContextReferenceContentNode - private let regularTextNode: ImmediateTextNode - private let whiteTextNode: ImmediateTextNode - private let iconNode: ASImageNode - private var animationNode: MoreIconNode? - - private var key: PeerInfoHeaderNavigationButtonKey? - private var theme: PresentationTheme? - - var isWhite: Bool = false { - didSet { - if self.isWhite != oldValue { - if case .qrCode = self.key, let theme = self.theme { - self.iconNode.image = self.isWhite ? generateTintedImage(image: PresentationResourcesRootController.navigationQrCodeIcon(theme), color: .white) : PresentationResourcesRootController.navigationQrCodeIcon(theme) - } else if case .postStory = self.key, let theme = self.theme { - self.iconNode.image = self.isWhite ? generateTintedImage(image: PresentationResourcesRootController.navigationPostStoryIcon(theme), color: .white) : PresentationResourcesRootController.navigationPostStoryIcon(theme) - } - - self.regularTextNode.isHidden = self.isWhite - self.whiteTextNode.isHidden = !self.isWhite - } - } - } - - var action: ((ASDisplayNode, ContextGesture?) -> Void)? - - init() { - self.contextSourceNode = ContextReferenceContentNode() - self.containerNode = ContextControllerSourceNode() - self.containerNode.animateScale = false - - self.regularTextNode = ImmediateTextNode() - self.whiteTextNode = ImmediateTextNode() - self.whiteTextNode.isHidden = true - - self.iconNode = ASImageNode() - self.iconNode.displaysAsynchronously = false - self.iconNode.displayWithoutProcessing = true - - super.init(pointerStyle: .insetRectangle(-8.0, 2.0)) - - self.isAccessibilityElement = true - self.accessibilityTraits = .button - - self.containerNode.addSubnode(self.contextSourceNode) - self.contextSourceNode.addSubnode(self.regularTextNode) - self.contextSourceNode.addSubnode(self.whiteTextNode) - self.contextSourceNode.addSubnode(self.iconNode) - - self.addSubnode(self.containerNode) - - self.containerNode.activated = { [weak self] gesture, _ in - guard let strongSelf = self else { - return - } - strongSelf.action?(strongSelf.contextSourceNode, gesture) - } - - self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) - } - - @objc private func pressed() { - self.animationNode?.play() - self.action?(self.contextSourceNode, nil) - } - - func update(key: PeerInfoHeaderNavigationButtonKey, presentationData: PresentationData, height: CGFloat) -> CGSize { - let textSize: CGSize - let isFirstTime = self.key == nil - if self.key != key || self.theme !== presentationData.theme { - self.key = key - self.theme = presentationData.theme - - let text: String - var accessibilityText: String - var icon: UIImage? - var isBold = false - var isGestureEnabled = false - var isAnimation = false - var animationState: MoreIconNodeState = .more - switch key { - case .edit: - text = presentationData.strings.Common_Edit - accessibilityText = text - case .done, .cancel, .selectionDone: - text = presentationData.strings.Common_Done - accessibilityText = text - isBold = true - case .select: - text = presentationData.strings.Common_Select - accessibilityText = text - case .search: - text = "" - accessibilityText = presentationData.strings.Common_Search - icon = nil// PresentationResourcesRootController.navigationCompactSearchIcon(presentationData.theme) - isAnimation = true - animationState = .search - case .editPhoto: - text = presentationData.strings.Settings_EditPhoto - accessibilityText = text - case .editVideo: - text = presentationData.strings.Settings_EditVideo - accessibilityText = text - case .more: - text = "" - accessibilityText = presentationData.strings.Common_More - icon = nil// PresentationResourcesRootController.navigationMoreCircledIcon(presentationData.theme) - isGestureEnabled = true - isAnimation = true - animationState = .more - case .qrCode: - text = "" - accessibilityText = presentationData.strings.PeerInfo_QRCode_Title - icon = PresentationResourcesRootController.navigationQrCodeIcon(presentationData.theme) - case .moreToSearch: - text = "" - accessibilityText = "" - case .postStory: - text = "" - accessibilityText = presentationData.strings.Story_Privacy_PostStory - icon = PresentationResourcesRootController.navigationPostStoryIcon(presentationData.theme) - } - self.accessibilityLabel = accessibilityText - self.containerNode.isGestureEnabled = isGestureEnabled - - let font: UIFont = isBold ? Font.semibold(17.0) : Font.regular(17.0) - - self.regularTextNode.attributedText = NSAttributedString(string: text, font: font, textColor: presentationData.theme.rootController.navigationBar.accentTextColor) - self.whiteTextNode.attributedText = NSAttributedString(string: text, font: font, textColor: .white) - self.iconNode.image = icon - - if isAnimation { - self.iconNode.isHidden = true - let animationNode: MoreIconNode - if let current = self.animationNode { - animationNode = current - } else { - animationNode = MoreIconNode() - self.animationNode = animationNode - self.contextSourceNode.addSubnode(animationNode) - } - animationNode.customColor = presentationData.theme.rootController.navigationBar.accentTextColor - animationNode.enqueueState(animationState, animated: !isFirstTime) - } else { - self.iconNode.isHidden = false - if let current = self.animationNode { - self.animationNode = nil - current.removeFromSupernode() - } - } - - textSize = self.regularTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) - let _ = self.whiteTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) - } else { - textSize = self.regularTextNode.bounds.size - } - - let inset: CGFloat = 0.0 - - let textFrame = CGRect(origin: CGPoint(x: inset, y: floor((height - textSize.height) / 2.0)), size: textSize) - self.regularTextNode.frame = textFrame - self.whiteTextNode.frame = textFrame - - if let animationNode = self.animationNode { - let animationSize = CGSize(width: 30.0, height: 30.0) - - animationNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((height - animationSize.height) / 2.0)), size: animationSize) - - let size = CGSize(width: animationSize.width + inset * 2.0, height: height) - self.containerNode.frame = CGRect(origin: CGPoint(), size: size) - self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size) - return size - } else if let image = self.iconNode.image { - self.iconNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((height - image.size.height) / 2.0)), size: image.size) - - let size = CGSize(width: image.size.width + inset * 2.0, height: height) - self.containerNode.frame = CGRect(origin: CGPoint(), size: size) - self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size) - return size - } else { - let size = CGSize(width: textSize.width + inset * 2.0, height: height) - self.containerNode.frame = CGRect(origin: CGPoint(), size: size) - self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size) - return size - } - } -} - -enum PeerInfoHeaderNavigationButtonKey { - case edit - case done - case cancel - case select - case selectionDone - case search - case editPhoto - case editVideo - case more - case qrCode - case moreToSearch - case postStory -} - -struct PeerInfoHeaderNavigationButtonSpec: Equatable { - let key: PeerInfoHeaderNavigationButtonKey - let isForExpandedView: Bool -} - -final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { - private var presentationData: PresentationData? - private(set) var leftButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:] - private(set) var rightButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:] - - private var currentLeftButtons: [PeerInfoHeaderNavigationButtonSpec] = [] - private var currentRightButtons: [PeerInfoHeaderNavigationButtonSpec] = [] - - var isWhite: Bool = false { - didSet { - if self.isWhite != oldValue { - for (_, buttonNode) in self.leftButtonNodes { - buttonNode.isWhite = self.isWhite - } - for (_, buttonNode) in self.rightButtonNodes { - buttonNode.isWhite = self.isWhite - } - } - } - } - - var performAction: ((PeerInfoHeaderNavigationButtonKey, ContextReferenceContentNode?, ContextGesture?) -> Void)? - - func update(size: CGSize, presentationData: PresentationData, leftButtons: [PeerInfoHeaderNavigationButtonSpec], rightButtons: [PeerInfoHeaderNavigationButtonSpec], expandFraction: CGFloat, transition: ContainedViewLayoutTransition) { - let maximumExpandOffset: CGFloat = 14.0 - let expandOffset: CGFloat = -expandFraction * maximumExpandOffset - - if self.currentLeftButtons != leftButtons || presentationData.strings !== self.presentationData?.strings { - self.currentLeftButtons = leftButtons - - var nextRegularButtonOrigin = 16.0 - var nextExpandedButtonOrigin = 16.0 - for spec in leftButtons.reversed() { - let buttonNode: PeerInfoHeaderNavigationButton - var wasAdded = false - if let current = self.leftButtonNodes[spec.key] { - buttonNode = current - } else { - wasAdded = true - buttonNode = PeerInfoHeaderNavigationButton() - self.leftButtonNodes[spec.key] = buttonNode - self.addSubnode(buttonNode) - buttonNode.action = { [weak self] _, gesture in - guard let strongSelf = self, let buttonNode = strongSelf.leftButtonNodes[spec.key] else { - return - } - strongSelf.performAction?(spec.key, buttonNode.contextSourceNode, gesture) - } - } - let buttonSize = buttonNode.update(key: spec.key, presentationData: presentationData, height: size.height) - var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin - let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) - nextButtonOrigin += buttonSize.width + 4.0 - if spec.isForExpandedView { - nextExpandedButtonOrigin = nextButtonOrigin - } else { - nextRegularButtonOrigin = nextButtonOrigin - } - let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) - if wasAdded { - buttonNode.frame = buttonFrame - buttonNode.alpha = 0.0 - transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) - - buttonNode.isWhite = self.isWhite - } else { - transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) - transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) - } - } - var removeKeys: [PeerInfoHeaderNavigationButtonKey] = [] - for (key, _) in self.leftButtonNodes { - if !leftButtons.contains(where: { $0.key == key }) { - removeKeys.append(key) - } - } - for key in removeKeys { - if let buttonNode = self.leftButtonNodes.removeValue(forKey: key) { - buttonNode.removeFromSupernode() - } - } - } else { - var nextRegularButtonOrigin = 16.0 - var nextExpandedButtonOrigin = 16.0 - for spec in leftButtons.reversed() { - if let buttonNode = self.leftButtonNodes[spec.key] { - let buttonSize = buttonNode.bounds.size - var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin - let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) - nextButtonOrigin += buttonSize.width + 4.0 - if spec.isForExpandedView { - nextExpandedButtonOrigin = nextButtonOrigin - } else { - nextRegularButtonOrigin = nextButtonOrigin - } - transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) - let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) - - var buttonTransition = transition - if case let .animated(duration, curve) = buttonTransition, alphaFactor == 0.0 { - buttonTransition = .animated(duration: duration * 0.25, curve: curve) - } - buttonTransition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) - } - } - } - - if self.currentRightButtons != rightButtons || presentationData.strings !== self.presentationData?.strings { - self.currentRightButtons = rightButtons - - var nextRegularButtonOrigin = size.width - 16.0 - var nextExpandedButtonOrigin = size.width - 16.0 - for spec in rightButtons.reversed() { - let buttonNode: PeerInfoHeaderNavigationButton - var wasAdded = false - - var key = spec.key - if key == .more || key == .search { - key = .moreToSearch - } - - if let current = self.rightButtonNodes[key] { - buttonNode = current - } else { - wasAdded = true - buttonNode = PeerInfoHeaderNavigationButton() - self.rightButtonNodes[key] = buttonNode - self.addSubnode(buttonNode) - } - buttonNode.action = { [weak self] _, gesture in - guard let strongSelf = self, let buttonNode = strongSelf.rightButtonNodes[key] else { - return - } - strongSelf.performAction?(spec.key, buttonNode.contextSourceNode, gesture) - } - let buttonSize = buttonNode.update(key: spec.key, presentationData: presentationData, height: size.height) - var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin - var buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin - buttonSize.width, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) - if case .postStory = spec.key { - buttonFrame.origin.x -= 12.0 - } - nextButtonOrigin -= buttonSize.width + 4.0 - if spec.isForExpandedView { - nextExpandedButtonOrigin = nextButtonOrigin - } else { - nextRegularButtonOrigin = nextButtonOrigin - } - let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) - if wasAdded { - buttonNode.isWhite = self.isWhite - - if key == .moreToSearch { - buttonNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) - } - - buttonNode.frame = buttonFrame - buttonNode.alpha = 0.0 - transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) - } else { - transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) - transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) - } - } - var removeKeys: [PeerInfoHeaderNavigationButtonKey] = [] - for (key, _) in self.rightButtonNodes { - if key == .moreToSearch { - if !rightButtons.contains(where: { $0.key == .more || $0.key == .search }) { - removeKeys.append(key) - } - } else if !rightButtons.contains(where: { $0.key == key }) { - removeKeys.append(key) - } - } - for key in removeKeys { - if let buttonNode = self.rightButtonNodes.removeValue(forKey: key) { - if key == .moreToSearch { - buttonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak buttonNode] _ in - buttonNode?.removeFromSupernode() - }) - buttonNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) - } else { - buttonNode.removeFromSupernode() - } - } - } - } else { - var nextRegularButtonOrigin = size.width - 16.0 - var nextExpandedButtonOrigin = size.width - 16.0 - - for spec in rightButtons.reversed() { - var key = spec.key - if key == .more || key == .search { - key = .moreToSearch - } - - if let buttonNode = self.rightButtonNodes[key] { - let buttonSize = buttonNode.bounds.size - var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin - var buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin - buttonSize.width, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) - if case .postStory = spec.key { - buttonFrame.origin.x -= 12.0 - } - nextButtonOrigin -= buttonSize.width + 4.0 - if spec.isForExpandedView { - nextExpandedButtonOrigin = nextButtonOrigin - } else { - nextRegularButtonOrigin = nextButtonOrigin - } - transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) - let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) - - var buttonTransition = transition - if case let .animated(duration, curve) = buttonTransition, alphaFactor == 0.0 { - buttonTransition = .animated(duration: duration * 0.25, curve: curve) - } - buttonTransition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) - } - } - } - self.presentationData = presentationData - } -} - final class PeerInfoHeaderRegularContentNode: ASDisplayNode { - } enum PeerInfoHeaderTextFieldNodeKey: Equatable { @@ -1847,544 +71,6 @@ protocol PeerInfoHeaderTextFieldNode: ASDisplayNode { func update(width: CGFloat, safeInset: CGFloat, isSettings: Bool, hasPrevious: Bool, hasNext: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat } -final class PeerInfoHeaderSingleLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode, UITextFieldDelegate { - private let backgroundNode: ASDisplayNode - private let textNode: TextFieldNode - private let measureTextNode: ImmediateTextNode - private let clearIconNode: ASImageNode - private let clearButtonNode: HighlightableButtonNode - private let topSeparator: ASDisplayNode - private let maskNode: ASImageNode - - private var theme: PresentationTheme? - - var text: String { - return self.textNode.textField.text ?? "" - } - - override init() { - self.backgroundNode = ASDisplayNode() - - self.textNode = TextFieldNode() - self.measureTextNode = ImmediateTextNode() - self.measureTextNode.maximumNumberOfLines = 0 - - self.clearIconNode = ASImageNode() - self.clearIconNode.isLayerBacked = true - self.clearIconNode.displayWithoutProcessing = true - self.clearIconNode.displaysAsynchronously = false - self.clearIconNode.isHidden = true - - self.clearButtonNode = HighlightableButtonNode() - self.clearButtonNode.isHidden = true - self.clearButtonNode.isAccessibilityElement = false - - self.topSeparator = ASDisplayNode() - - self.maskNode = ASImageNode() - self.maskNode.isUserInteractionEnabled = false - - super.init() - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textNode) - self.addSubnode(self.clearIconNode) - self.addSubnode(self.clearButtonNode) - self.addSubnode(self.topSeparator) - self.addSubnode(self.maskNode) - - self.textNode.textField.delegate = self - - self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside) - self.clearButtonNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.clearIconNode.layer.removeAnimation(forKey: "opacity") - strongSelf.clearIconNode.alpha = 0.4 - } else { - strongSelf.clearIconNode.alpha = 1.0 - strongSelf.clearIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - } - } - - @objc private func clearButtonPressed() { - self.textNode.textField.text = "" - self.updateClearButtonVisibility() - } - - @objc func textFieldDidBeginEditing(_ textField: UITextField) { - self.updateClearButtonVisibility() - } - - @objc func textFieldDidEndEditing(_ textField: UITextField) { - self.updateClearButtonVisibility() - } - - private func updateClearButtonVisibility() { - let isHidden = !self.textNode.textField.isFirstResponder || self.text.isEmpty - self.clearIconNode.isHidden = isHidden - self.clearButtonNode.isHidden = isHidden - self.clearButtonNode.isAccessibilityElement = isHidden - } - - func update(width: CGFloat, safeInset: CGFloat, isSettings: Bool, hasPrevious: Bool, hasNext: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat { - let titleFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize) - self.textNode.textField.font = titleFont - - if self.theme !== presentationData.theme { - self.theme = presentationData.theme - - self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor - - self.textNode.textField.textColor = presentationData.theme.list.itemPrimaryTextColor - self.textNode.textField.keyboardAppearance = presentationData.theme.rootController.keyboardColor.keyboardAppearance - self.textNode.textField.tintColor = presentationData.theme.list.itemAccentColor - - self.clearIconNode.image = PresentationResourcesItemList.itemListClearInputIcon(presentationData.theme) - } - - let attributedPlaceholderText = NSAttributedString(string: placeholder, font: titleFont, textColor: presentationData.theme.list.itemPlaceholderTextColor) - if self.textNode.textField.attributedPlaceholder == nil || !self.textNode.textField.attributedPlaceholder!.isEqual(to: attributedPlaceholderText) { - self.textNode.textField.attributedPlaceholder = attributedPlaceholderText - self.textNode.textField.accessibilityHint = attributedPlaceholderText.string - } - - if let updateText = updateText { - self.textNode.textField.text = updateText - } - - if !hasPrevious { - self.topSeparator.isHidden = true - } - self.topSeparator.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor - let separatorX = safeInset + (hasPrevious ? 16.0 : 0.0) - self.topSeparator.frame = CGRect(origin: CGPoint(x: separatorX, y: 0.0), size: CGSize(width: width - separatorX - safeInset, height: UIScreenPixel)) - - let measureText = "|" - let attributedMeasureText = NSAttributedString(string: measureText, font: titleFont, textColor: .black) - self.measureTextNode.attributedText = attributedMeasureText - let measureTextSize = self.measureTextNode.updateLayout(CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: .greatestFiniteMagnitude)) - - let height = measureTextSize.height + 22.0 - - let buttonSize = CGSize(width: 38.0, height: height) - self.clearButtonNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width, y: 0.0), size: buttonSize) - if let image = self.clearIconNode.image { - self.clearIconNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width + floor((buttonSize.width - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size) - } - - self.backgroundNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: max(1.0, width - safeInset * 2.0), height: height)) - self.textNode.frame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: floor((height - 40.0) / 2.0)), size: CGSize(width: max(1.0, width - safeInset * 2.0 - 16.0 * 2.0 - 38.0), height: 40.0)) - - let hasCorners = safeInset > 0.0 && (!hasPrevious || !hasNext) - let hasTopCorners = hasCorners && !hasPrevious - let hasBottomCorners = hasCorners && !hasNext - - self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil - self.maskNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: width - safeInset - safeInset, height: height)) - - self.textNode.isUserInteractionEnabled = isEnabled - self.textNode.alpha = isEnabled ? 1.0 : 0.6 - - return height - } -} - -final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode, ASEditableTextNodeDelegate { - private let backgroundNode: ASDisplayNode - private let textNode: EditableTextNode - private let textNodeContainer: ASDisplayNode - private let measureTextNode: ImmediateTextNode - private let clearIconNode: ASImageNode - private let clearButtonNode: HighlightableButtonNode - private let topSeparator: ASDisplayNode - private let maskNode: ASImageNode - - private let requestUpdateHeight: () -> Void - - private var fontSize: PresentationFontSize? - private var theme: PresentationTheme? - private var currentParams: (width: CGFloat, safeInset: CGFloat)? - private var currentMeasuredHeight: CGFloat? - - var text: String { - return self.textNode.attributedText?.string ?? "" - } - - init(requestUpdateHeight: @escaping () -> Void) { - self.requestUpdateHeight = requestUpdateHeight - - self.backgroundNode = ASDisplayNode() - - self.textNode = EditableTextNode() - self.textNode.clipsToBounds = false - self.textNode.textView.clipsToBounds = false - self.textNode.textContainerInset = UIEdgeInsets() - - self.textNodeContainer = ASDisplayNode() - self.measureTextNode = ImmediateTextNode() - self.measureTextNode.maximumNumberOfLines = 0 - self.measureTextNode.isUserInteractionEnabled = false - self.measureTextNode.lineSpacing = 0.1 - self.topSeparator = ASDisplayNode() - - self.clearIconNode = ASImageNode() - self.clearIconNode.isLayerBacked = true - self.clearIconNode.displayWithoutProcessing = true - self.clearIconNode.displaysAsynchronously = false - self.clearIconNode.isHidden = true - - self.clearButtonNode = HighlightableButtonNode() - self.clearButtonNode.isHidden = true - self.clearButtonNode.isAccessibilityElement = false - - self.maskNode = ASImageNode() - self.maskNode.isUserInteractionEnabled = false - - super.init() - - self.addSubnode(self.backgroundNode) - self.textNodeContainer.addSubnode(self.textNode) - self.addSubnode(self.textNodeContainer) - self.addSubnode(self.clearIconNode) - self.addSubnode(self.clearButtonNode) - self.addSubnode(self.topSeparator) - self.addSubnode(self.maskNode) - - self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside) - self.clearButtonNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.clearIconNode.layer.removeAnimation(forKey: "opacity") - strongSelf.clearIconNode.alpha = 0.4 - } else { - strongSelf.clearIconNode.alpha = 1.0 - strongSelf.clearIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - } - } - - @objc private func clearButtonPressed() { - guard let theme = self.theme else { - return - } - let font: UIFont - if let fontSize = self.fontSize { - font = Font.regular(fontSize.itemListBaseFontSize) - } else { - font = Font.regular(17.0) - } - let attributedText = NSAttributedString(string: "", font: font, textColor: theme.list.itemPrimaryTextColor) - self.textNode.attributedText = attributedText - self.requestUpdateHeight() - self.updateClearButtonVisibility() - } - - func update(width: CGFloat, safeInset: CGFloat, isSettings: Bool, hasPrevious: Bool, hasNext: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat { - self.currentParams = (width, safeInset) - - self.fontSize = presentationData.listsFontSize - let titleFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize) - - if self.theme !== presentationData.theme { - self.theme = presentationData.theme - - self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor - - let textColor = presentationData.theme.list.itemPrimaryTextColor - self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: titleFont, NSAttributedString.Key.foregroundColor.rawValue: textColor] - self.textNode.keyboardAppearance = presentationData.theme.rootController.keyboardColor.keyboardAppearance - self.textNode.tintColor = presentationData.theme.list.itemAccentColor - - self.textNode.clipsToBounds = true - self.textNode.delegate = self - self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) - - self.clearIconNode.image = PresentationResourcesItemList.itemListClearInputIcon(presentationData.theme) - } - - self.topSeparator.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor - - let separatorX = safeInset + (hasPrevious ? 16.0 : 0.0) - self.topSeparator.frame = CGRect(origin: CGPoint(x: separatorX, y: 0.0), size: CGSize(width: width - separatorX - safeInset, height: UIScreenPixel)) - - let attributedPlaceholderText = NSAttributedString(string: placeholder, font: titleFont, textColor: presentationData.theme.list.itemPlaceholderTextColor) - if self.textNode.attributedPlaceholderText == nil || !self.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) { - self.textNode.attributedPlaceholderText = attributedPlaceholderText - } - - if let updateText = updateText { - let attributedText = NSAttributedString(string: updateText, font: titleFont, textColor: presentationData.theme.list.itemPrimaryTextColor) - self.textNode.attributedText = attributedText - } - - var measureText = self.textNode.attributedText?.string ?? "" - if measureText.hasSuffix("\n") || measureText.isEmpty { - measureText += "|" - } - let attributedMeasureText = NSAttributedString(string: measureText, font: titleFont, textColor: .gray) - self.measureTextNode.attributedText = attributedMeasureText - let measureTextSize = self.measureTextNode.updateLayout(CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: .greatestFiniteMagnitude)) - self.measureTextNode.frame = CGRect(origin: CGPoint(), size: measureTextSize) - self.currentMeasuredHeight = measureTextSize.height - - let height = measureTextSize.height + 22.0 - - let buttonSize = CGSize(width: 38.0, height: height) - self.clearButtonNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width, y: 0.0), size: buttonSize) - if let image = self.clearIconNode.image { - self.clearIconNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width + floor((buttonSize.width - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size) - } - - let textNodeFrame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: 10.0), size: CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: max(height, 1000.0))) - self.textNodeContainer.frame = textNodeFrame - self.textNode.frame = CGRect(origin: CGPoint(), size: textNodeFrame.size) - - self.backgroundNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: max(1.0, width - safeInset * 2.0), height: height)) - - let hasCorners = safeInset > 0.0 && (!hasPrevious || !hasNext) - let hasTopCorners = hasCorners && !hasPrevious - let hasBottomCorners = hasCorners && !hasNext - - self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil - self.maskNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: width - safeInset - safeInset, height: height)) - - return height - } - - func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { - self.updateClearButtonVisibility() - } - - func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { - self.updateClearButtonVisibility() - } - - private func updateClearButtonVisibility() { - let isHidden = !self.textNode.isFirstResponder() || self.text.isEmpty - self.clearIconNode.isHidden = isHidden - self.clearButtonNode.isHidden = isHidden - self.clearButtonNode.isAccessibilityElement = isHidden - } - - func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - guard let theme = self.theme else { - return true - } - let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) - if updatedText.count > 255 { - let attributedText = NSAttributedString(string: String(updatedText[updatedText.startIndex.. 0.1 { - self.requestUpdateHeight() - } - } - } - - func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { - let text: String? = UIPasteboard.general.string - if let _ = text { - return true - } else { - return false - } - } -} - -final class PeerInfoHeaderEditingContentNode: ASDisplayNode { - private let context: AccountContext - private let requestUpdateLayout: () -> Void - - var requestEditing: (() -> Void)? - - let avatarNode: PeerInfoEditingAvatarNode - let avatarTextNode: ImmediateTextNode - let avatarButtonNode: HighlightableButtonNode - - var itemNodes: [PeerInfoHeaderTextFieldNodeKey: PeerInfoHeaderTextFieldNode] = [:] - - init(context: AccountContext, requestUpdateLayout: @escaping () -> Void) { - self.context = context - self.requestUpdateLayout = requestUpdateLayout - - self.avatarNode = PeerInfoEditingAvatarNode(context: context) - - self.avatarTextNode = ImmediateTextNode() - self.avatarButtonNode = HighlightableButtonNode() - - super.init() - - self.addSubnode(self.avatarNode) - self.avatarButtonNode.addSubnode(self.avatarTextNode) - - self.avatarButtonNode.addTarget(self, action: #selector(textPressed), forControlEvents: .touchUpInside) - } - - @objc private func textPressed() { - self.requestEditing?() - } - - func editingTextForKey(_ key: PeerInfoHeaderTextFieldNodeKey) -> String? { - return self.itemNodes[key]?.text - } - - func shakeTextForKey(_ key: PeerInfoHeaderTextFieldNodeKey) { - self.itemNodes[key]?.layer.addShakeAnimation() - } - - func update(width: CGFloat, safeInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, peer: Peer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, cachedData: CachedPeerData?, isContact: Bool, isSettings: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat { - let avatarSize: CGFloat = isModalOverlay ? 200.0 : 100.0 - let avatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize) / 2.0), y: statusBarHeight + 22.0), size: CGSize(width: avatarSize, height: avatarSize)) - transition.updateFrameAdditiveToCenter(node: self.avatarNode, frame: CGRect(origin: avatarFrame.center, size: CGSize())) - - var contentHeight: CGFloat = statusBarHeight + 10.0 + avatarSize + 20.0 - - if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) { - if self.avatarButtonNode.supernode == nil { - self.addSubnode(self.avatarButtonNode) - } - self.avatarTextNode.attributedText = NSAttributedString(string: presentationData.strings.Settings_SetNewProfilePhotoOrVideo, font: Font.regular(17.0), textColor: presentationData.theme.list.itemAccentColor) - self.avatarButtonNode.accessibilityLabel = self.avatarTextNode.attributedText?.string - - let avatarTextSize = self.avatarTextNode.updateLayout(CGSize(width: width, height: 32.0)) - transition.updateFrame(node: self.avatarTextNode, frame: CGRect(origin: CGPoint(), size: avatarTextSize)) - transition.updateFrame(node: self.avatarButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - avatarTextSize.width) / 2.0), y: contentHeight - 1.0), size: avatarTextSize)) - contentHeight += 32.0 - } - - var isEditableBot = false - if let user = peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { - isEditableBot = true - } - var fieldKeys: [PeerInfoHeaderTextFieldNodeKey] = [] - if let user = peer as? TelegramUser { - if !user.isDeleted { - fieldKeys.append(.firstName) - if isEditableBot { - fieldKeys.append(.description) - } else if user.botInfo == nil { - fieldKeys.append(.lastName) - } - } - } else if let _ = peer as? TelegramGroup { - fieldKeys.append(.title) - if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) { - fieldKeys.append(.description) - } - } else if let _ = peer as? TelegramChannel { - fieldKeys.append(.title) - if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) { - fieldKeys.append(.description) - } - } - var hasPrevious = false - for key in fieldKeys { - let itemNode: PeerInfoHeaderTextFieldNode - var updateText: String? - if let current = self.itemNodes[key] { - itemNode = current - } else { - var isMultiline = false - switch key { - case .firstName: - if let peer = peer as? TelegramUser { - if let editableBotInfo = (cachedData as? CachedUserData)?.editableBotInfo { - updateText = editableBotInfo.name - } else { - updateText = peer.firstName ?? "" - } - } - case .lastName: - updateText = (peer as? TelegramUser)?.lastName ?? "" - case .title: - updateText = peer?.debugDisplayTitle ?? "" - case .description: - isMultiline = true - if let cachedData = cachedData as? CachedChannelData { - updateText = cachedData.about ?? "" - } else if let cachedData = cachedData as? CachedGroupData { - updateText = cachedData.about ?? "" - } else if let cachedData = cachedData as? CachedUserData { - if let editableBotInfo = cachedData.editableBotInfo { - updateText = editableBotInfo.about - } else { - updateText = cachedData.about ?? "" - } - } else { - updateText = "" - } - } - if isMultiline { - itemNode = PeerInfoHeaderMultiLineTextFieldNode(requestUpdateHeight: { [weak self] in - self?.requestUpdateLayout() - }) - } else { - itemNode = PeerInfoHeaderSingleLineTextFieldNode() - } - self.itemNodes[key] = itemNode - self.addSubnode(itemNode) - } - let placeholder: String - var isEnabled = true - switch key { - case .firstName: - placeholder = isEditableBot ? presentationData.strings.UserInfo_BotNamePlaceholder : presentationData.strings.UserInfo_FirstNamePlaceholder - isEnabled = isContact || isSettings || isEditableBot - case .lastName: - placeholder = presentationData.strings.UserInfo_LastNamePlaceholder - isEnabled = isContact || isSettings - case .title: - if let channel = peer as? TelegramChannel, case .broadcast = channel.info { - placeholder = presentationData.strings.GroupInfo_ChannelListNamePlaceholder - } else { - placeholder = presentationData.strings.GroupInfo_GroupNamePlaceholder - } - isEnabled = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) - case .description: - placeholder = presentationData.strings.Channel_Edit_AboutItem - isEnabled = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) || isEditableBot - } - let itemHeight = itemNode.update(width: width, safeInset: safeInset, isSettings: isSettings, hasPrevious: hasPrevious, hasNext: key != fieldKeys.last, placeholder: placeholder, isEnabled: isEnabled, presentationData: presentationData, updateText: updateText) - transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight))) - contentHeight += itemHeight - hasPrevious = true - } - var removeKeys: [PeerInfoHeaderTextFieldNodeKey] = [] - for (key, _) in self.itemNodes { - if !fieldKeys.contains(key) { - removeKeys.append(key) - } - } - for key in removeKeys { - if let itemNode = self.itemNodes.removeValue(forKey: key) { - itemNode.removeFromSupernode() - } - } - - return contentHeight - } -} - private let TitleNodeStateRegular = 0 private let TitleNodeStateExpanded = 1 @@ -2409,6 +95,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { let avatarClippingNode: SparseNode let avatarListNode: PeerInfoAvatarListNode + let backgroundBannerView: UIView + let backgroundCover = ComponentView() let buttonsContainerNode: SparseNode let regularContentNode: PeerInfoHeaderRegularContentNode let editingContentNode: PeerInfoHeaderEditingContentNode @@ -2511,6 +199,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.usernameNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) self.usernameNode.displaysAsynchronously = false + self.backgroundBannerView = UIView() + self.backgroundBannerView.clipsToBounds = true + self.backgroundBannerView.isUserInteractionEnabled = false + self.buttonsContainerNode = SparseNode() self.buttonsContainerNode.clipsToBounds = true @@ -2556,11 +248,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { self?.requestUpdateLayout?(false) } - if !isMediaOnly { - self.addSubnode(self.buttonsContainerNode) - } self.addSubnode(self.backgroundNode) self.addSubnode(self.expandedBackgroundNode) + self.view.addSubview(self.backgroundBannerView) self.titleNodeContainer.addSubnode(self.titleNode) self.subtitleNodeContainer.addSubnode(self.subtitleNode) self.subtitleNodeContainer.addSubnode(self.panelSubtitleNode) @@ -2577,6 +267,11 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.regularContentNode.addSubnode(self.usernameNodeRawContainer) self.addSubnode(self.regularContentNode) + + if !isMediaOnly { + self.regularContentNode.addSubnode(self.buttonsContainerNode) + } + self.addSubnode(self.editingContentNode) self.addSubnode(self.avatarOverlayNode) self.addSubnode(self.navigationBackgroundNode) @@ -2797,7 +492,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { emojiRegularStatusContent = .none emojiExpandedStatusContent = .none case .premium: - emojiRegularStatusContent = .premium(color: presentationData.theme.list.itemAccentColor) + emojiRegularStatusContent = .premium(color: UIColor.white) emojiExpandedStatusContent = .premium(color: UIColor(rgb: 0xffffff, alpha: 0.75)) case .verified: emojiRegularStatusContent = .verified(fillColor: presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .large) @@ -2810,8 +505,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { emojiExpandedStatusContent = emojiRegularStatusContent case let .emojiStatus(emojiStatus): currentEmojiStatus = emojiStatus - emojiRegularStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: presentationData.theme.list.mediaPlaceholderColor, themeColor: presentationData.theme.list.itemAccentColor, loopMode: .forever) - emojiExpandedStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.15), themeColor: presentationData.theme.list.itemAccentColor, loopMode: .forever) + emojiRegularStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: presentationData.theme.list.mediaPlaceholderColor, themeColor: UIColor.white, loopMode: .forever) + emojiExpandedStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.15), themeColor: UIColor.white, loopMode: .forever) } let animateStatusIcon = !self.titleCredibilityIconView.bounds.isEmpty @@ -2897,7 +592,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { } self.regularContentNode.alpha = state.isEditing ? 0.0 : 1.0 - self.buttonsContainerNode.alpha = self.regularContentNode.alpha self.editingContentNode.alpha = state.isEditing ? 1.0 : 0.0 let editingContentHeight = self.editingContentNode.update(width: width, safeInset: containerInset, statusBarHeight: statusBarHeight, navigationHeight: navigationHeight, isModalOverlay: isModalOverlay, peer: state.isEditing ? peer : nil, threadData: threadData, chatLocation: self.chatLocation, cachedData: cachedData, isContact: isContact, isSettings: isSettings, presentationData: presentationData, transition: transition) @@ -2917,6 +611,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.backgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) let headerBackgroundColor: UIColor = presentationData.theme.list.blocksBackgroundColor + let _ = headerBackgroundColor + var effectiveSeparatorAlpha: CGFloat if let navigationTransition = self.navigationTransition { transitionSourceHeight = navigationTransition.sourceNavigationBar.backgroundNode.bounds.height @@ -2939,6 +635,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { transitionSourceTitleFrame = navigationTransition.sourceTitleFrame transitionSourceSubtitleFrame = navigationTransition.sourceSubtitleFrame + transition.updateAlpha(layer: self.backgroundBannerView.layer, alpha: 1.0 - transitionFraction) + self.expandedBackgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor.mixedWith(headerBackgroundColor, alpha: 1.0 - transitionFraction), forceKeepBlur: true, transition: transition) effectiveSeparatorAlpha = transitionFraction @@ -2949,8 +647,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { } else { let contentOffset = max(0.0, contentOffset - 140.0) let backgroundTransitionFraction: CGFloat = max(0.0, min(1.0, contentOffset / 30.0)) - self.expandedBackgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.mixedWith(headerBackgroundColor, alpha: 1.0 - backgroundTransitionFraction), forceKeepBlur: true, transition: transition) + transition.updateAlpha(layer: self.backgroundBannerView.layer, alpha: state.isEditing ? 0.0 : 1.0) + effectiveSeparatorAlpha = backgroundTransitionFraction self.avatarClippingNode.clipsToBounds = true @@ -2961,7 +660,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.avatarOverlayNode.updateTransitionFraction(transitionFraction, transition: transition) if self.navigationTitle != presentationData.strings.EditProfile_Title || themeUpdated { - self.navigationTitleNode.attributedText = NSAttributedString(string: presentationData.strings.EditProfile_Title, font: Font.semibold(17.0), textColor: presentationData.theme.rootController.navigationBar.primaryTextColor) + self.navigationTitleNode.attributedText = NSAttributedString(string: presentationData.strings.EditProfile_Title, font: Font.semibold(17.0), textColor: UIColor.white) } let navigationTitleSize = self.navigationTitleNode.updateLayout(CGSize(width: width, height: navigationHeight)) @@ -3028,8 +727,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { } titleStringText = title - titleAttributes = MultiScaleTextState.Attributes(font: Font.medium(30.0), color: presentationData.theme.list.itemPrimaryTextColor) - smallTitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(30.0), color: .white) + titleAttributes = MultiScaleTextState.Attributes(font: Font.medium(28.0), color: UIColor.white) + smallTitleAttributes = MultiScaleTextState.Attributes(font: Font.medium(28.0), color: .white) if self.isSettings, let user = peer as? TelegramUser { var subtitle = formatPhoneNumber(context: self.context, number: user.phone ?? "") @@ -3038,22 +737,22 @@ final class PeerInfoHeaderNode: ASDisplayNode { subtitle = "\(subtitle) • @\(mainUsername)" } subtitleStringText = subtitle - subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(17.0), color: presentationData.theme.list.itemSecondaryTextColor) - smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: UIColor(white: 1.0, alpha: 0.7)) + subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(17.0), color: UIColor.white.withMultipliedAlpha(0.7)) + smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(16.0), color: UIColor(white: 1.0, alpha: 0.7)) - usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)) + usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(16.0), color: UIColor.white.withMultipliedAlpha(0.7))) } else if let _ = threadData { let subtitleColor: UIColor - subtitleColor = presentationData.theme.list.itemAccentColor + subtitleColor = UIColor.white let statusText: String statusText = peer.debugDisplayTitle subtitleStringText = statusText - subtitleAttributes = MultiScaleTextState.Attributes(font: Font.semibold(15.0), color: subtitleColor) - smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: UIColor(white: 1.0, alpha: 0.7)) + subtitleAttributes = MultiScaleTextState.Attributes(font: Font.semibold(16.0), color: subtitleColor) + smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(16.0), color: UIColor(white: 1.0, alpha: 0.7)) - usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)) + usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(16.0), color: UIColor.white.withMultipliedAlpha(0.7))) subtitleIsButton = true @@ -3061,59 +760,59 @@ final class PeerInfoHeaderNode: ASDisplayNode { if let panelStatusData = maybePanelStatusData { let subtitleColor: UIColor if panelStatusData.isActivity { - subtitleColor = presentationData.theme.list.itemAccentColor + subtitleColor = UIColor.white } else { - subtitleColor = presentationData.theme.list.itemSecondaryTextColor + subtitleColor = UIColor.white.withAlphaComponent(0.7) } panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor)) } if let nextPanelStatusData = maybeNextPanelStatusData { - nextPanelSubtitleString = (nextPanelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: presentationData.theme.list.itemSecondaryTextColor)) + nextPanelSubtitleString = (nextPanelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: UIColor.white.withMultipliedAlpha(0.7))) } } else if let statusData = statusData { let subtitleColor: UIColor if statusData.isActivity { - subtitleColor = presentationData.theme.list.itemAccentColor + subtitleColor = UIColor.white } else { - subtitleColor = presentationData.theme.list.itemSecondaryTextColor + subtitleColor = UIColor.white.withMultipliedAlpha(0.7) } subtitleStringText = statusData.text subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor) - smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: UIColor(white: 1.0, alpha: 0.7)) + smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(16.0), color: UIColor(white: 1.0, alpha: 0.7)) - usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)) + usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(16.0), color: UIColor.white.withMultipliedAlpha(0.7))) let (maybePanelStatusData, maybeNextPanelStatusData, _) = panelStatusData if let panelStatusData = maybePanelStatusData { let subtitleColor: UIColor if panelStatusData.isActivity { - subtitleColor = presentationData.theme.list.itemAccentColor + subtitleColor = UIColor.white } else { - subtitleColor = presentationData.theme.list.itemSecondaryTextColor + subtitleColor = UIColor.white.withMultipliedAlpha(0.7) } panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor)) } if let nextPanelStatusData = maybeNextPanelStatusData { - nextPanelSubtitleString = (nextPanelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: presentationData.theme.list.itemSecondaryTextColor)) + nextPanelSubtitleString = (nextPanelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: UIColor.white.withMultipliedAlpha(0.7))) } } else { subtitleStringText = " " - subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor) - smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor) + subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(16.0), color: UIColor.white.withMultipliedAlpha(0.7)) + smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(16.0), color: UIColor.white.withMultipliedAlpha(0.7)) - usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)) + usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(16.0), color: UIColor.white.withMultipliedAlpha(0.7))) } } else { titleStringText = " " - titleAttributes = MultiScaleTextState.Attributes(font: Font.regular(24.0), color: presentationData.theme.list.itemPrimaryTextColor) + titleAttributes = MultiScaleTextState.Attributes(font: Font.regular(24.0), color: UIColor.white) smallTitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(24.0), color: .white) subtitleStringText = " " - subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor) - smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor) + subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(16.0), color: UIColor.white.withMultipliedAlpha(0.7)) + smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(16.0), color: UIColor.white.withMultipliedAlpha(0.7)) - usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)) + usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(16.0), color: UIColor.white.withMultipliedAlpha(0.7))) } let textSideInset: CGFloat = 36.0 @@ -3169,16 +868,16 @@ final class PeerInfoHeaderNode: ASDisplayNode { if let current = self.subtitleArrowNode { subtitleArrowNode = current if themeUpdated { - subtitleArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.5)) + subtitleArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: UIColor.white.withMultipliedAlpha(0.5)) } } else { subtitleArrowNode = ASImageNode() self.subtitleArrowNode = subtitleArrowNode self.subtitleNode.insertSubnode(subtitleArrowNode, at: 1) - subtitleArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.5)) + subtitleArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: UIColor.white.withMultipliedAlpha(0.5)) } - subtitleBackgroundNode.backgroundColor = presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.1) + subtitleBackgroundNode.backgroundColor = UIColor.white.withMultipliedAlpha(0.1) let subtitleSize = subtitleNodeLayout[TitleNodeStateRegular]!.size var subtitleBackgroundFrame = CGRect(origin: CGPoint(), size: subtitleSize).offsetBy(dx: -subtitleSize.width * 0.5, dy: -subtitleSize.height * 0.5).insetBy(dx: -6.0, dy: -4.0) subtitleBackgroundFrame.size.width += 12.0 @@ -3275,11 +974,16 @@ final class PeerInfoHeaderNode: ASDisplayNode { let usernameFrame: CGRect let usernameSpacing: CGFloat = 4.0 + let expandedTitleScale: CGFloat = 0.8 + transition.updateFrame(node: self.avatarListNode.listContainerNode.bottomShadowNode, frame: CGRect(origin: CGPoint(x: 0.0, y: expandedAvatarHeight - 70.0), size: CGSize(width: width, height: 70.0))) if self.isAvatarExpanded { - let minTitleSize = CGSize(width: titleSize.width * 0.7, height: titleSize.height * 0.7) - let minTitleFrame = CGRect(origin: CGPoint(x: 16.0, y: expandedAvatarHeight - 58.0 - UIScreenPixel + (subtitleSize.height.isZero ? 10.0 : 0.0)), size: minTitleSize) + let minTitleSize = CGSize(width: titleSize.width * expandedTitleScale, height: titleSize.height * expandedTitleScale) + var minTitleFrame = CGRect(origin: CGPoint(x: 16.0, y: expandedAvatarHeight - 58.0 - UIScreenPixel + (subtitleSize.height.isZero ? 10.0 : 0.0)), size: minTitleSize) + if !self.isSettings { + minTitleFrame.origin.y -= 83.0 + } titleFrame = CGRect(origin: CGPoint(x: minTitleFrame.midX - titleSize.width / 2.0, y: minTitleFrame.midY - titleSize.height / 2.0), size: titleSize) subtitleFrame = CGRect(origin: CGPoint(x: 16.0, y: minTitleFrame.maxY + 2.0), size: subtitleSize) @@ -3597,24 +1301,39 @@ final class PeerInfoHeaderNode: ASDisplayNode { let rawHeight: CGFloat let height: CGFloat let maxY: CGFloat + let backgroundHeight: CGFloat if self.isAvatarExpanded { rawHeight = expandedAvatarHeight height = max(navigationHeight, rawHeight - contentOffset) - maxY = height + maxY = height - 98.0 + backgroundHeight = height } else { rawHeight = navigationHeight + panelWithAvatarHeight - height = navigationHeight + max(0.0, panelWithAvatarHeight - contentOffset) + var expandablePart: CGFloat = panelWithAvatarHeight - contentOffset + if self.isSettings { + expandablePart += 20.0 + } else { + if peer?.id == self.context.account.peerId { + expandablePart = 0.0 + } else { + expandablePart += 99.0 + } + } + height = navigationHeight + max(0.0, expandablePart) maxY = navigationHeight + panelWithAvatarHeight - contentOffset + backgroundHeight = height } + let _ = maxY - let apparentHeight = (1.0 - transitionFraction) * height + transitionFraction * transitionSourceHeight + let apparentHeight = (1.0 - transitionFraction) * backgroundHeight + transitionFraction * transitionSourceHeight + let apparentBackgroundHeight = (1.0 - transitionFraction) * backgroundHeight + transitionFraction * transitionSourceHeight if !titleSize.width.isZero && !titleSize.height.isZero { if self.navigationTransition != nil { var neutralTitleScale: CGFloat = 1.0 var neutralSubtitleScale: CGFloat = 1.0 if self.isAvatarExpanded { - neutralTitleScale = 0.7 + neutralTitleScale = expandedTitleScale neutralSubtitleScale = 1.0 } @@ -3648,7 +1367,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let subtitleScale: CGFloat var subtitleOffset: CGFloat = 0.0 if self.isAvatarExpanded { - titleScale = 0.7 + titleScale = expandedTitleScale subtitleScale = 1.0 } else { titleScale = (1.0 - titleCollapseFraction) * 1.0 + titleCollapseFraction * titleMinScale @@ -3692,9 +1411,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { let buttonSpacing: CGFloat = 8.0 let buttonSideInset = max(16.0, containerInset) - var actionButtonRightOrigin = CGPoint(x: width - buttonSideInset, y: maxY + 24.0 - navigationHeight - UIScreenPixel) let actionButtonWidth = (width - buttonSideInset * 2.0 + buttonSpacing) / CGFloat(actionButtonKeys.count) - buttonSpacing let actionButtonSize = CGSize(width: actionButtonWidth, height: 40.0) + var actionButtonRightOrigin = CGPoint(x: width - buttonSideInset, y: backgroundHeight - 16.0 - actionButtonSize.height) for buttonKey in actionButtonKeys.reversed() { let buttonNode: PeerInfoHeaderActionButtonNode @@ -3748,12 +1467,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } - var buttonRightOrigin = CGPoint(x: width - buttonSideInset, y: maxY + 24.0 - navigationHeight - UIScreenPixel) + let buttonWidth = (width - buttonSideInset * 2.0 + buttonSpacing) / CGFloat(buttonKeys.count) - buttonSpacing + let buttonSize = CGSize(width: buttonWidth, height: 58.0) + var buttonRightOrigin = CGPoint(x: width - buttonSideInset, y: backgroundHeight - 16.0 - buttonSize.height) if !actionButtonKeys.isEmpty { buttonRightOrigin.y += actionButtonSize.height + 24.0 } - let buttonWidth = (width - buttonSideInset * 2.0 + buttonSpacing) / CGFloat(buttonKeys.count) - buttonSpacing - let buttonSize = CGSize(width: buttonWidth, height: 58.0) for buttonKey in buttonKeys.reversed() { let buttonNode: PeerInfoHeaderButtonNode @@ -3885,7 +1604,37 @@ final class PeerInfoHeaderNode: ASDisplayNode { } transition.updateFrame(node: self.regularContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: resolvedHeight))) - transition.updateFrame(node: self.buttonsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight + UIScreenPixel), size: CGSize(width: width, height: resolvedHeight - navigationHeight + 500.0))) + transition.updateFrameAdditive(node: self.buttonsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: apparentBackgroundHeight - backgroundHeight), size: CGSize(width: width, height: 1000.0))) + let buttonsTransitionDistance: CGFloat = -min(0.0, apparentBackgroundHeight - backgroundHeight) + let buttonsTransitionDistanceNorm: CGFloat = 40.0 + + let innerContentOffset = max(0.0, contentOffset - 140.0) + let backgroundTransitionFraction: CGFloat = 1.0 - max(0.0, min(1.0, innerContentOffset / 30.0)) + + let buttonsTransitionFraction: CGFloat = 1.0 - max(0.0, min(1.0, buttonsTransitionDistance / buttonsTransitionDistanceNorm)) + transition.updateAlpha(node: self.buttonsContainerNode, alpha: buttonsTransitionFraction * backgroundTransitionFraction) + + let bannerFrame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0 + apparentBackgroundHeight), size: CGSize(width: width, height: 2000.0)) + transition.updateFrameAdditive(view: self.backgroundBannerView, frame: bannerFrame) + let backgroundCoverSize = self.backgroundCover.update( + transition: Transition(transition), + component: AnyComponent(PeerInfoCoverComponent( + context: self.context, + peer: peer.flatMap(EnginePeer.init), + avatarCenter: apparentAvatarFrame.center, + avatarScale: avatarScale, + avatarTransitionFraction: max(0.0, min(1.0, titleCollapseFraction + transitionFraction * 2.0)), + patternTransitionFraction: buttonsTransitionFraction * backgroundTransitionFraction + )), + environment: {}, + containerSize: CGSize(width: width, height: apparentBackgroundHeight) + ) + if let backgroundCoverView = self.backgroundCover.view { + if backgroundCoverView.superview == nil { + self.backgroundBannerView.addSubview(backgroundCoverView) + } + transition.updateFrameAdditive(view: backgroundCoverView, frame: CGRect(origin: CGPoint(x: 0.0, y: bannerFrame.height - backgroundCoverSize.height), size: backgroundCoverSize)) + } if additive { transition.updateFrameAdditive(node: self.backgroundNode, frame: backgroundFrame) @@ -3901,11 +1650,21 @@ final class PeerInfoHeaderNode: ASDisplayNode { transition.updateFrame(node: self.separatorNode, frame: separatorFrame) } - if !state.isEditing && !isSettings { - resolvedHeight += 71.0 - - if !actionButtonKeys.isEmpty { - resolvedHeight += 64.0 + if !state.isEditing { + if !isSettings { + if self.isAvatarExpanded { + resolvedHeight -= 21.0 + } else { + resolvedHeight += 79.0 + + if !actionButtonKeys.isEmpty { + resolvedHeight += 64.0 + } + } + } else { + if self.isAvatarExpanded { + resolvedHeight -= 21.0 + } } } @@ -3962,6 +1721,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { return result } + if let result = self.buttonsContainerNode.view.hitTest(self.view.convert(point, to: self.buttonsContainerNode.view), with: event) { + return result + } + if result == self.view || result == self.regularContentNode.view || result == self.editingContentNode.view { return nil } @@ -3997,129 +1760,3 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } -private class DynamicIslandMaskNode: ASDisplayNode { - var animationNode: AnimationNode? - - var isForum = false { - didSet { - if self.isForum != oldValue { - self.animationNode?.removeFromSupernode() - let animationNode = AnimationNode(animation: "ForumAvatarMask") - self.addSubnode(animationNode) - self.animationNode = animationNode - } - } - } - - override init() { - let animationNode = AnimationNode(animation: "UserAvatarMask") - self.animationNode = animationNode - - super.init() - - self.addSubnode(animationNode) - } - - func update(_ value: CGFloat) { - self.animationNode?.setProgress(value) - } - - var animating = false - - override func layout() { - self.animationNode?.frame = self.bounds - } -} - -private class DynamicIslandBlurNode: ASDisplayNode { - private var effectView: UIVisualEffectView? - private let fadeNode = ASDisplayNode() - let gradientNode = ASImageNode() - - private var hierarchyTrackingNode: HierarchyTrackingNode? - - deinit { - self.animator?.stopAnimation(true) - } - - override func didLoad() { - super.didLoad() - - let hierarchyTrackingNode = HierarchyTrackingNode({ [weak self] value in - if !value { - self?.animator?.stopAnimation(true) - self?.animator = nil - } - }) - self.hierarchyTrackingNode = hierarchyTrackingNode - self.addSubnode(hierarchyTrackingNode) - - self.fadeNode.backgroundColor = .black - self.fadeNode.alpha = 0.0 - - self.gradientNode.displaysAsynchronously = false - let gradientImage = generateImage(CGSize(width: 100.0, height: 100.0), rotatedContext: { size, context in - let bounds = CGRect(origin: .zero, size: size) - context.clear(bounds) - - var locations: [CGFloat] = [0.0, 0.87, 1.0] - let colors: [CGColor] = [UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 1.0).cgColor] - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! - - let endRadius: CGFloat = 90.0 - let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0 + 38.0) - context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endRadius, options: .drawsAfterEndLocation) - }) - self.gradientNode.image = gradientImage - - let effectView = UIVisualEffectView(effect: nil) - self.effectView = effectView - self.view.insertSubview(effectView, at: 0) - - self.addSubnode(self.gradientNode) - self.addSubnode(self.fadeNode) - } - - private var animator: UIViewPropertyAnimator? - - func prepare() -> Bool { - guard self.animator == nil else { - return false - } - let animator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) - self.animator = animator - self.effectView?.effect = nil - animator.addAnimations { [weak self] in - self?.effectView?.effect = UIBlurEffect(style: .dark) - } - return true - } - - func update(_ value: CGFloat) { - let fadeAlpha = min(1.0, max(0.0, -0.25 + value * 1.55)) - if value > 0.0 { - var value = value - let updated = self.prepare() - if value > 0.99 && updated { - value = 0.99 - } - self.animator?.fractionComplete = max(0.0, -0.1 + value * 1.1) - } else { - self.animator?.stopAnimation(true) - self.animator = nil - self.effectView?.effect = nil - } - self.fadeNode.alpha = fadeAlpha - } - - override func layout() { - super.layout() - - self.effectView?.frame = self.bounds - self.fadeNode.frame = self.bounds - - let gradientSize = CGSize(width: 100.0, height: 100.0) - self.gradientNode.frame = CGRect(origin: CGPoint(x: (self.bounds.width - gradientSize.width) / 2.0, y: 0.0), size: gradientSize) - } -} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderSingleLineTextFieldNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderSingleLineTextFieldNode.swift new file mode 100644 index 0000000000..95638a8cd3 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderSingleLineTextFieldNode.swift @@ -0,0 +1,151 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import ContextUI +import TelegramPresentationData +import Display + +final class PeerInfoHeaderSingleLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode, UITextFieldDelegate { + private let backgroundNode: ASDisplayNode + private let textNode: TextFieldNode + private let measureTextNode: ImmediateTextNode + private let clearIconNode: ASImageNode + private let clearButtonNode: HighlightableButtonNode + private let topSeparator: ASDisplayNode + private let maskNode: ASImageNode + + private var theme: PresentationTheme? + + var text: String { + return self.textNode.textField.text ?? "" + } + + override init() { + self.backgroundNode = ASDisplayNode() + + self.textNode = TextFieldNode() + self.measureTextNode = ImmediateTextNode() + self.measureTextNode.maximumNumberOfLines = 0 + + self.clearIconNode = ASImageNode() + self.clearIconNode.isLayerBacked = true + self.clearIconNode.displayWithoutProcessing = true + self.clearIconNode.displaysAsynchronously = false + self.clearIconNode.isHidden = true + + self.clearButtonNode = HighlightableButtonNode() + self.clearButtonNode.isHidden = true + self.clearButtonNode.isAccessibilityElement = false + + self.topSeparator = ASDisplayNode() + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textNode) + self.addSubnode(self.clearIconNode) + self.addSubnode(self.clearButtonNode) + self.addSubnode(self.topSeparator) + self.addSubnode(self.maskNode) + + self.textNode.textField.delegate = self + + self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside) + self.clearButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.clearIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.clearIconNode.alpha = 0.4 + } else { + strongSelf.clearIconNode.alpha = 1.0 + strongSelf.clearIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + @objc private func clearButtonPressed() { + self.textNode.textField.text = "" + self.updateClearButtonVisibility() + } + + @objc func textFieldDidBeginEditing(_ textField: UITextField) { + self.updateClearButtonVisibility() + } + + @objc func textFieldDidEndEditing(_ textField: UITextField) { + self.updateClearButtonVisibility() + } + + private func updateClearButtonVisibility() { + let isHidden = !self.textNode.textField.isFirstResponder || self.text.isEmpty + self.clearIconNode.isHidden = isHidden + self.clearButtonNode.isHidden = isHidden + self.clearButtonNode.isAccessibilityElement = isHidden + } + + func update(width: CGFloat, safeInset: CGFloat, isSettings: Bool, hasPrevious: Bool, hasNext: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat { + let titleFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize) + self.textNode.textField.font = titleFont + + if self.theme !== presentationData.theme { + self.theme = presentationData.theme + + self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor + + self.textNode.textField.textColor = presentationData.theme.list.itemPrimaryTextColor + self.textNode.textField.keyboardAppearance = presentationData.theme.rootController.keyboardColor.keyboardAppearance + self.textNode.textField.tintColor = presentationData.theme.list.itemAccentColor + + self.clearIconNode.image = PresentationResourcesItemList.itemListClearInputIcon(presentationData.theme) + } + + let attributedPlaceholderText = NSAttributedString(string: placeholder, font: titleFont, textColor: presentationData.theme.list.itemPlaceholderTextColor) + if self.textNode.textField.attributedPlaceholder == nil || !self.textNode.textField.attributedPlaceholder!.isEqual(to: attributedPlaceholderText) { + self.textNode.textField.attributedPlaceholder = attributedPlaceholderText + self.textNode.textField.accessibilityHint = attributedPlaceholderText.string + } + + if let updateText = updateText { + self.textNode.textField.text = updateText + } + + if !hasPrevious { + self.topSeparator.isHidden = true + } + self.topSeparator.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + let separatorX = safeInset + (hasPrevious ? 16.0 : 0.0) + self.topSeparator.frame = CGRect(origin: CGPoint(x: separatorX, y: 0.0), size: CGSize(width: width - separatorX - safeInset, height: UIScreenPixel)) + + let measureText = "|" + let attributedMeasureText = NSAttributedString(string: measureText, font: titleFont, textColor: .black) + self.measureTextNode.attributedText = attributedMeasureText + let measureTextSize = self.measureTextNode.updateLayout(CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: .greatestFiniteMagnitude)) + + let height = measureTextSize.height + 22.0 + + let buttonSize = CGSize(width: 38.0, height: height) + self.clearButtonNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width, y: 0.0), size: buttonSize) + if let image = self.clearIconNode.image { + self.clearIconNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width + floor((buttonSize.width - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size) + } + + self.backgroundNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: max(1.0, width - safeInset * 2.0), height: height)) + self.textNode.frame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: floor((height - 40.0) / 2.0)), size: CGSize(width: max(1.0, width - safeInset * 2.0 - 16.0 * 2.0 - 38.0), height: 40.0)) + + let hasCorners = safeInset > 0.0 && (!hasPrevious || !hasNext) + let hasTopCorners = hasCorners && !hasPrevious + let hasBottomCorners = hasCorners && !hasNext + + self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + self.maskNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: width - safeInset - safeInset, height: height)) + + self.textNode.isUserInteractionEnabled = isEnabled + self.textNode.alpha = isEnabled ? 1.0 : 0.6 + + return height + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index c770e0661f..55ded97264 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -2979,9 +2979,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.addSubnode(self.scrollNode) self.scrollNode.addSubnode(self.paneContainerNode) - if !self.isMediaOnly { - self.addSubnode(self.headerNode.buttonsContainerNode) - } self.addSubnode(self.headerNode) self.scrollNode.view.isScrollEnabled = !self.isMediaOnly @@ -9951,7 +9948,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.paneContainerNode.update(size: self.paneContainerNode.bounds.size, sideInset: layout.safeInsets.left, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: effectiveAreaExpansionFraction, presentationData: self.presentationData, data: self.data, transition: transition) transition.updateFrame(node: self.headerNode.navigationButtonContainer, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left, y: layout.statusBarHeight ?? 0.0), size: CGSize(width: layout.size.width - layout.safeInsets.left * 2.0, height: navigationBarHeight))) - self.headerNode.navigationButtonContainer.isWhite = self.headerNode.isAvatarExpanded + self.headerNode.navigationButtonContainer.isWhite = true//self.headerNode.isAvatarExpanded var leftNavigationButtons: [PeerInfoHeaderNavigationButtonSpec] = [] var rightNavigationButtons: [PeerInfoHeaderNavigationButtonSpec] = [] @@ -10095,7 +10092,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func updateNavigationExpansionPresentation(isExpanded: Bool, animated: Bool) { if let controller = self.controller { - controller.setStatusBarStyle(isExpanded ? .White : self.presentationData.theme.rootController.statusBarStyle.style, animated: animated) + controller.setStatusBarStyle(.White, animated: animated) if animated { UIView.transition(with: controller.controllerNode.headerNode.navigationButtonContainer.view, duration: 0.3, options: [.transitionCrossDissolve], animations: { @@ -10105,7 +10102,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let baseNavigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) let navigationBarPresentationData = NavigationBarPresentationData( theme: NavigationBarTheme( - buttonColor: isExpanded ? .white : baseNavigationBarPresentationData.theme.buttonColor, + buttonColor: .white, disabledButtonColor: baseNavigationBarPresentationData.theme.disabledButtonColor, primaryTextColor: baseNavigationBarPresentationData.theme.primaryTextColor, backgroundColor: .clear, @@ -10273,7 +10270,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc let baseNavigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) super.init(navigationBarPresentationData: NavigationBarPresentationData( theme: NavigationBarTheme( - buttonColor: avatarInitiallyExpanded ? .white : baseNavigationBarPresentationData.theme.buttonColor, + buttonColor: .white, disabledButtonColor: baseNavigationBarPresentationData.theme.disabledButtonColor, primaryTextColor: baseNavigationBarPresentationData.theme.primaryTextColor, backgroundColor: .clear, @@ -10494,7 +10491,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } } - self.setStatusBarStyle(avatarInitiallyExpanded ? .White : self.presentationData.theme.rootController.statusBarStyle.style, animated: false) + self.setStatusBarStyle(.White, animated: false) self.scrollToTop = { [weak self] in self?.controllerNode.scrollToTop() @@ -10991,7 +10988,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig } } - if self.screenNode.headerNode.isAvatarExpanded, let currentBackButtonArrow = topNavigationBar.makeTransitionBackArrowNode(accentColor: self.screenNode.headerNode.isAvatarExpanded ? .white : self.presentationData.theme.rootController.navigationBar.accentTextColor) { + if let currentBackButtonArrow = topNavigationBar.makeTransitionBackArrowNode(accentColor: .white) { self.currentBackButtonArrow = currentBackButtonArrow self.addSubnode(currentBackButtonArrow) } @@ -11005,7 +11002,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig } } - if let currentBackButton = topNavigationBar.makeTransitionBackButtonNode(accentColor: self.screenNode.headerNode.isAvatarExpanded ? .white : self.presentationData.theme.rootController.navigationBar.accentTextColor) { + if let currentBackButton = topNavigationBar.makeTransitionBackButtonNode(accentColor: .white) { self.currentBackButton = currentBackButton self.addSubnode(currentBackButton) } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift index 0ebd20b6f7..4878d848c5 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift @@ -1054,23 +1054,7 @@ final class PieChartComponent: Component { self.backgroundColor = nil self.isOpaque = false - var previousTimestamp: Double? - self.displayLink = SharedDisplayLinkDriver.shared.add(needsHighestFramerate: true, { [weak self] in - let timestamp = CACurrentMediaTime() - var delta: Double - if let previousTimestamp { - delta = timestamp - previousTimestamp - } else { - delta = 1.0 / 60.0 - } - previousTimestamp = timestamp - - if delta < 0.0 { - delta = 1.0 / 60.0 - } else if delta > 0.5 { - delta = 1.0 / 60.0 - } - + self.displayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] delta in self?.update(deltaTime: CGFloat(delta)) }) } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 35c1572211..6e888d6ec3 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3074,6 +3074,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) }, action: { [weak self] controller, f in if let strongSelf = self { + if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { + strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds([id]) + } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: [id], type: .forLocalPeer).startStandalone() } f(.dismissWithoutContent) @@ -8552,9 +8555,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } if isAction && (actions.options == .deleteGlobally || actions.options == .deleteLocally) { + if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { + strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds) + } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: actions.options == .deleteLocally ? .forLocalPeer : .forEveryone).startStandalone() completion(.dismissWithoutContent) } else if (messages.first?.flags.isSending ?? false) { + if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { + strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds) + } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone, deleteAllInGroup: true).startStandalone() completion(.dismissWithoutContent) } else { @@ -17002,6 +17011,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = strongSelf.context.engine.messages.deleteAllMessagesWithAuthor(peerId: peerId, authorId: author.id, namespace: Namespaces.Message.Cloud).startStandalone() let _ = strongSelf.context.engine.messages.clearAuthorHistory(peerId: peerId, memberId: author.id).startStandalone() } else if actions.contains(0) { + if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { + strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds) + } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } if actions.contains(1) { @@ -17040,6 +17052,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { + strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds) + } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } })) @@ -17077,6 +17092,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let commit = { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { + strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds) + } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } if let giveaway { @@ -17098,6 +17116,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { + strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds) + } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } })) @@ -17139,7 +17160,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { + strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds) + } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone() + } })) } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index d857527e9d..05986022f0 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -2866,11 +2866,19 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let completion: (Bool, ListViewDisplayedItemRange) -> Void = { [weak self] wasTransformed, visibleRange in if let strongSelf = self { var newIncomingReactions: [MessageId: (value: MessageReaction.Reaction, isLarge: Bool)] = [:] + var expiredMessageIds = Set() + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent()) + if case .peer = strongSelf.chatLocation, let previousHistoryView = strongSelf.historyView { var updatedIncomingReactions: [MessageId: (value: MessageReaction.Reaction, isLarge: Bool)] = [:] + var existingIds = Set() for entry in transition.historyView.filteredEntries { switch entry { case let .MessageEntry(message, _, _, _, _, _): + if message.autoremoveAttribute != nil { + existingIds.insert(message.id) + } + if message.flags.contains(.Incoming) { continue } @@ -2883,6 +2891,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } case let .MessageGroupEntry(_, messages, _): for message in messages { + if message.0.autoremoveAttribute != nil { + existingIds.insert(message.0.id) + } + if message.0.flags.contains(.Incoming) { continue } @@ -2914,6 +2926,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto newIncomingReactions[message.id] = updatedReaction } } + if !existingIds.contains(message.id) { + if let autoremoveAttribute = message.autoremoveAttribute, let countdownBeginTime = autoremoveAttribute.countdownBeginTime { + let exipiresAt = countdownBeginTime + autoremoveAttribute.timeout + if exipiresAt >= currentTimestamp - 1 { + expiredMessageIds.insert(message.id) + } + } + } case let .MessageGroupEntry(_, messages, _): for message in messages { if let updatedReaction = updatedIncomingReactions[message.0.id] { @@ -2929,6 +2949,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto newIncomingReactions[message.0.id] = updatedReaction } } + if !existingIds.contains(message.0.id) { + if let autoremoveAttribute = message.0.autoremoveAttribute, let countdownBeginTime = autoremoveAttribute.countdownBeginTime { + let exipiresAt = countdownBeginTime + autoremoveAttribute.timeout + if exipiresAt >= currentTimestamp - 1 { + expiredMessageIds.insert(message.0.id) + } + } + } } default: break @@ -3172,12 +3200,18 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } + var dustMessageIds = Set() + dustMessageIds.formUnion(expiredMessageIds) if let currentDeleteAnimationCorrelationIds = strongSelf.currentDeleteAnimationCorrelationIds { + dustMessageIds.formUnion(currentDeleteAnimationCorrelationIds) + } + + if !dustMessageIds.isEmpty { var foundItemNodes: [ChatMessageItemView] = [] strongSelf.forEachRemovedItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { for (message, _) in item.content { - if currentDeleteAnimationCorrelationIds.contains(message.id) { + if dustMessageIds.contains(message.id) { foundItemNodes.append(itemNode) } } @@ -3185,32 +3219,30 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } if !foundItemNodes.isEmpty { strongSelf.currentDeleteAnimationCorrelationIds = nil - if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { - if strongSelf.dustEffectLayer == nil { - let dustEffectLayer = DustEffectLayer() - dustEffectLayer.position = strongSelf.bounds.center - dustEffectLayer.bounds = CGRect(origin: CGPoint(), size: strongSelf.bounds.size) - strongSelf.dustEffectLayer = dustEffectLayer - dustEffectLayer.zPosition = 10.0 - dustEffectLayer.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) - strongSelf.layer.addSublayer(dustEffectLayer) - dustEffectLayer.becameEmpty = { [weak strongSelf] in - guard let strongSelf else { - return - } - strongSelf.dustEffectLayer?.removeFromSuperlayer() - strongSelf.dustEffectLayer = nil + if strongSelf.dustEffectLayer == nil { + let dustEffectLayer = DustEffectLayer() + dustEffectLayer.position = strongSelf.bounds.center + dustEffectLayer.bounds = CGRect(origin: CGPoint(), size: strongSelf.bounds.size) + strongSelf.dustEffectLayer = dustEffectLayer + dustEffectLayer.zPosition = 10.0 + dustEffectLayer.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) + strongSelf.layer.addSublayer(dustEffectLayer) + dustEffectLayer.becameEmpty = { [weak strongSelf] in + guard let strongSelf else { + return } + strongSelf.dustEffectLayer?.removeFromSuperlayer() + strongSelf.dustEffectLayer = nil } - if let dustEffectLayer = strongSelf.dustEffectLayer { - for itemNode in foundItemNodes { - guard let (image, subFrame) = itemNode.makeContentSnapshot() else { - continue - } - let itemFrame = itemNode.layer.convert(subFrame, to: dustEffectLayer) - dustEffectLayer.addItem(frame: itemFrame, image: image) - itemNode.isHidden = true + } + if let dustEffectLayer = strongSelf.dustEffectLayer { + for itemNode in foundItemNodes { + guard let (image, subFrame) = itemNode.makeContentSnapshot() else { + continue } + let itemFrame = itemNode.layer.convert(subFrame, to: dustEffectLayer) + dustEffectLayer.addItem(frame: itemFrame, image: image) + itemNode.isHidden = true } } } diff --git a/submodules/Utils/LokiRng/BUILD b/submodules/Utils/LokiRng/BUILD new file mode 100644 index 0000000000..ac5a34a442 --- /dev/null +++ b/submodules/Utils/LokiRng/BUILD @@ -0,0 +1,24 @@ + +objc_library( + name = "LokiRng", + enable_modules = True, + module_name = "LokiRng", + srcs = glob([ + "Sources/**/*.m", + "Sources/**/*.mm", + "Sources/**/*.h", + "Sources/**/*.cpp", + ]), + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + sdk_frameworks = [ + "Foundation", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h b/submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h new file mode 100644 index 0000000000..e1c0abb7c3 --- /dev/null +++ b/submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h @@ -0,0 +1,14 @@ +#ifndef LokiRng_h +#define LokiRng_h + +#import + +@interface LokiRng : NSObject + +- (instancetype _Nonnull)initWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2; + +- (float)next; + +@end + +#endif /* LokiRng_h */ diff --git a/submodules/Utils/LokiRng/Sources/LokiRng.mm b/submodules/Utils/LokiRng/Sources/LokiRng.mm new file mode 100644 index 0000000000..3f8240d361 --- /dev/null +++ b/submodules/Utils/LokiRng/Sources/LokiRng.mm @@ -0,0 +1,64 @@ +#import + +static uint32_t tausStep(const uint32_t z, const int32_t s1, const int32_t s2, const int32_t s3, const uint32_t M) { + uint32_t b = (((z << s1) ^ z) >> s2); + return (((z & M) << s3) ^ b); +} + +@interface LokiRng () { + float _seed; +} + +@end + +@implementation LokiRng + +- (instancetype _Nonnull)initWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2 { + self = [super init]; + if (self != nil) { + uint32_t seed = ((uint32_t)seed0) * 1099087573U; + uint32_t seedb = ((uint32_t)seed1) * 1099087573U; + uint32_t seedc = ((uint32_t)seed2) * 1099087573U; + + // Round 1: Randomise seed + uint32_t z1 = tausStep(seed,13,19,12,429496729U); + uint32_t z2 = tausStep(seed,2,25,4,4294967288U); + uint32_t z3 = tausStep(seed,3,11,17,429496280U); + uint32_t z4 = (1664525*seed + 1013904223U); + + // Round 2: Randomise seed again using second seed + uint32_t r1 = (z1^z2^z3^z4^seedb); + + z1 = tausStep(r1,13,19,12,429496729U); + z2 = tausStep(r1,2,25,4,4294967288U); + z3 = tausStep(r1,3,11,17,429496280U); + z4 = (1664525*r1 + 1013904223U); + + // Round 3: Randomise seed again using third seed + r1 = (z1^z2^z3^z4^seedc); + + z1 = tausStep(r1,13,19,12,429496729U); + z2 = tausStep(r1,2,25,4,4294967288U); + z3 = tausStep(r1,3,11,17,429496280U); + z4 = (1664525*r1 + 1013904223U); + + _seed = (z1^z2^z3^z4) * 2.3283064365387e-10f; + } + return self; +} + +- (float)next { + uint32_t hashed_seed = _seed * 1099087573U; + + uint32_t z1 = tausStep(hashed_seed,13,19,12,429496729U); + uint32_t z2 = tausStep(hashed_seed,2,25,4,4294967288U); + uint32_t z3 = tausStep(hashed_seed,3,11,17,429496280U); + uint32_t z4 = (1664525*hashed_seed + 1013904223U); + + float old_seed = _seed; + _seed = (z1^z2^z3^z4) * 2.3283064365387e-10f; + + return old_seed; +} + +@end diff --git a/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift b/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift index cc33f87ed8..c5a8656e86 100644 --- a/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift +++ b/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift @@ -402,7 +402,7 @@ final public class AnimationView: AnimationViewBase { if self.needsWorkaroundDisplayLink != oldValue { if self.needsWorkaroundDisplayLink { if self.workaroundDisplayLink == nil { - self.workaroundDisplayLink = SharedDisplayLinkDriver.shared.add { [weak self] in + self.workaroundDisplayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in let _ = self?.realtimeAnimationProgress } }