import Foundation import AsyncDisplayKit import Postbox import UIKit import Display import TelegramCore import SwiftSignalKit private class AvatarNodeParameters: NSObject { let account: Account let peerId: PeerId let letters: [String] let font: UIFont init(account: Account, peerId: PeerId, letters: [String], font: UIFont) { self.account = account self.peerId = peerId self.letters = letters self.font = font super.init() } } private let gradientColors: [NSArray] = [ [UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor], [UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor], [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor], [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor], [UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor], [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor], ] private let grayscaleColors: NSArray = [ UIColor(rgb: 0xefefef).cgColor, UIColor(rgb: 0xeeeeee).cgColor ] private enum AvatarNodeState: Equatable { case Empty case PeerAvatar(PeerId, [String], TelegramMediaImageRepresentation?) } private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool { switch (lhs, rhs) { case (.Empty, .Empty): return true case let (.PeerAvatar(lhsPeerId, lhsLetters, lhsPhotoRepresentations), .PeerAvatar(rhsPeerId, rhsLetters, rhsPhotoRepresentations)): return lhsPeerId == rhsPeerId && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations default: return false } } public final class AvatarNode: ASDisplayNode { var font: UIFont { didSet { if oldValue !== font { if let parameters = self.parameters { self.parameters = AvatarNodeParameters(account: parameters.account, peerId: parameters.peerId, letters: parameters.letters, font: self.font) } if !self.displaySuspended { self.setNeedsDisplay() } } } } private var parameters: AvatarNodeParameters? let imageNode: ImageNode private var state: AvatarNodeState = .Empty private let imageReady = Promise(false) public var ready: Signal { let imageReady = self.imageReady return Signal { subscriber in return imageReady.get().start(next: { next in if next { subscriber.putCompletion() } }) } } public init(font: UIFont) { self.font = font self.imageNode = ImageNode(enableHasImage: true) super.init() self.isOpaque = false self.displaysAsynchronously = true self.imageNode.isLayerBacked = true self.addSubnode(self.imageNode) } override public var frame: CGRect { get { return super.frame } set(value) { let updateImage = !value.size.equalTo(super.frame.size) super.frame = value self.imageNode.frame = CGRect(origin: CGPoint(), size: value.size) if updateImage && !self.displaySuspended { self.setNeedsDisplay() } } } public func setPeer(account: Account, peer: Peer, temporaryRepresentation: TelegramMediaImageRepresentation? = nil) { var representation: TelegramMediaImageRepresentation? if let temporaryRepresentation = temporaryRepresentation { representation = temporaryRepresentation } else { representation = peer.smallProfileImage } let updatedState = AvatarNodeState.PeerAvatar(peer.id, peer.displayLetters, representation) if updatedState != self.state { self.state = updatedState let parameters = AvatarNodeParameters(account: account, peerId: peer.id, letters: peer.displayLetters, font: self.font) self.displaySuspended = true self.contents = nil if let signal = peerAvatarImage(account: account, peer: peer, temporaryRepresentation: temporaryRepresentation) { self.imageReady.set(self.imageNode.ready) self.imageNode.setSignal(signal) } else { self.imageReady.set(.single(true)) self.displaySuspended = false } if self.parameters == nil || self.parameters != parameters { self.parameters = parameters self.setNeedsDisplay() } } } override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol { return parameters ?? NSObject() } @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { assertNotOnMainThread() let context = UIGraphicsGetCurrentContext()! if !isRasterizing { context.setBlendMode(.copy) context.setFillColor(UIColor.clear.cgColor) context.fill(bounds) } context.beginPath() context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: bounds.size.width, height: bounds.size.height)) context.clip() let colorIndex: Int if let parameters = parameters as? AvatarNodeParameters { if parameters.peerId.id == 0 { colorIndex = -1 } else { colorIndex = abs(Int(parameters.account.peerId.id + parameters.peerId.id)) } } else { colorIndex = -1 } let colorsArray: NSArray if colorIndex == -1 { colorsArray = grayscaleColors } else { colorsArray = gradientColors[colorIndex % gradientColors.count] } var locations: [CGFloat] = [1.0, 0.2]; let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)! context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.size.height), options: CGGradientDrawingOptions()) context.setBlendMode(.normal) if let parameters = parameters as? AvatarNodeParameters { let letters = parameters.letters let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) let attributedString = NSAttributedString(string: string, attributes: [NSAttributedStringKey.font: parameters.font, NSAttributedStringKey.foregroundColor: UIColor.white]) let line = CTLineCreateWithAttributedString(attributedString) let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0) let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (bounds.size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (bounds.size.height - lineBounds.size.height) / 2.0)) context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) context.translateBy(x: lineOrigin.x, y: lineOrigin.y) CTLineDraw(line, context) context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) } } static func asyncLayout(_ node: AvatarNode?) -> (_ account: Account, _ peer: Peer, _ font: UIFont) -> () -> AvatarNode? { let currentState = node?.state let createNode = node == nil return { [weak node] account, peer, font in let state: AvatarNodeState = .PeerAvatar(peer.id, peer.displayLetters, peer.smallProfileImage) if currentState != state { } var createdNode: AvatarNode? if createNode { createdNode = AvatarNode(font: font) } return { let updatedNode: AvatarNode? if let createdNode = createdNode { updatedNode = createdNode } else { updatedNode = node } if let updatedNode = updatedNode { return updatedNode } else { return nil } } } } }