[WIP] Colored profile banners

This commit is contained in:
Ali 2023-11-17 21:48:46 +04:00
parent 165b23570b
commit e25f926342
38 changed files with 3431 additions and 2656 deletions

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -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?()
}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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 {

View File

@ -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()
})
}
}
}
}

View File

@ -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

View File

@ -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",
],
)

View File

@ -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)
}
}

View File

@ -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",

View File

@ -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)
}
}

View File

@ -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()
})
})
}
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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))
})
}

View File

@ -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()
}
}))
}

View File

@ -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
}
}
}

View 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",
],
)

View 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 */

View 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

View File

@ -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
}
}