mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
[WIP] Colored profile banners
This commit is contained in:
parent
165b23570b
commit
e25f926342
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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?()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>(UInt32(particleColumnCount), UInt32(particleRowCount))
|
||||
computeEncoder.setBytes(&particleCount, length: 4 * 2, index: 1)
|
||||
var phase = item.phase
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -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<Empty>, 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<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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<Bool>()
|
||||
|
||||
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<Bool, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
|> then(.single(true)))
|
||||
|
||||
let galleryReady = self.listContainerNode.isReady.get()
|
||||
|> filter { value in
|
||||
return value
|
||||
}
|
||||
|> take(1)
|
||||
|
||||
let combinedSignal: Signal<Bool, NoError>
|
||||
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()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Empty>?
|
||||
var videoNode: UniversalVideoNode?
|
||||
var markupNode: AvatarVideoNode?
|
||||
var iconView: ComponentView<Empty>?
|
||||
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<String>()
|
||||
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<Empty>
|
||||
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)
|
||||
}
|
||||
}
|
@ -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<String>()
|
||||
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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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..<updatedText.index(updatedText.startIndex, offsetBy: 255)]), font: Font.regular(17.0), textColor: theme.list.itemPrimaryTextColor)
|
||||
self.textNode.attributedText = attributedText
|
||||
self.requestUpdateHeight()
|
||||
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
if let (width, safeInset) = self.currentParams {
|
||||
var measureText = self.textNode.attributedText?.string ?? ""
|
||||
if measureText.hasSuffix("\n") || measureText.isEmpty {
|
||||
measureText += "|"
|
||||
}
|
||||
let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), 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))
|
||||
if let currentMeasuredHeight = self.currentMeasuredHeight, abs(measureTextSize.height - currentMeasuredHeight) > 0.1 {
|
||||
self.requestUpdateHeight()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool {
|
||||
let text: String? = UIPasteboard.general.string
|
||||
if let _ = text {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
@ -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<MessageId>()
|
||||
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent())
|
||||
|
||||
if case .peer = strongSelf.chatLocation, let previousHistoryView = strongSelf.historyView {
|
||||
var updatedIncomingReactions: [MessageId: (value: MessageReaction.Reaction, isLarge: Bool)] = [:]
|
||||
var existingIds = Set<MessageId>()
|
||||
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<MessageId>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
submodules/Utils/LokiRng/BUILD
Normal file
24
submodules/Utils/LokiRng/BUILD
Normal file
@ -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",
|
||||
],
|
||||
)
|
14
submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h
Normal file
14
submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h
Normal file
@ -0,0 +1,14 @@
|
||||
#ifndef LokiRng_h
|
||||
#define LokiRng_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface LokiRng : NSObject
|
||||
|
||||
- (instancetype _Nonnull)initWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2;
|
||||
|
||||
- (float)next;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* LokiRng_h */
|
64
submodules/Utils/LokiRng/Sources/LokiRng.mm
Normal file
64
submodules/Utils/LokiRng/Sources/LokiRng.mm
Normal file
@ -0,0 +1,64 @@
|
||||
#import <LokiRng/LokiRng.h>
|
||||
|
||||
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
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user