import Foundation import AsyncDisplayKit import Postbox import UIKit import Display import TelegramCore import SwiftSignalKit private let savedMessagesIcon = UIImage(bundleImageName: "Avatar/SavedMessagesIcon")?.precomposed() private class AvatarNodeParameters: NSObject { let theme: PresentationTheme? let accountPeerId: PeerId? let peerId: PeerId? let letters: [String] let font: UIFont let icon: AvatarNodeIcon let explicitColorIndex: Int? let hasImage: Bool init(theme: PresentationTheme?, accountPeerId: PeerId?, peerId: PeerId?, letters: [String], font: UIFont, icon: AvatarNodeIcon, explicitColorIndex: Int?, hasImage: Bool) { self.theme = theme self.accountPeerId = accountPeerId self.peerId = peerId self.letters = letters self.font = font self.icon = icon self.explicitColorIndex = explicitColorIndex self.hasImage = hasImage super.init() } func withUpdatedHasImage(_ hasImage: Bool) -> AvatarNodeParameters { return AvatarNodeParameters(theme: self.theme, accountPeerId: self.accountPeerId, peerId: self.peerId, letters: self.letters, font: self.font, icon: self.icon, explicitColorIndex: self.explicitColorIndex, hasImage: hasImage) } } private let gradientColors: [NSArray] = [ [UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor], [UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor], [UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor], [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor], [UIColor(rgb: 0x4acccd).cgColor, UIColor(rgb: 0x00fcfd).cgColor], [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor], [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor], ] private let grayscaleColors: NSArray = [ UIColor(rgb: 0xb1b1b1).cgColor, UIColor(rgb: 0xcdcdcd).cgColor ] private let savedMessagesColors: NSArray = [ UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor ] private enum AvatarNodeState: Equatable { case empty case peerAvatar(PeerId, [String], TelegramMediaImageRepresentation?) case custom(letter: [String], explicitColorIndex: Int?) } 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 case let (.custom(lhsLetters, lhsIndex), .custom(rhsLetters, rhsIndex)): return lhsLetters == rhsLetters && lhsIndex == rhsIndex default: return false } } enum AvatarNodeIcon { case none case savedMessagesIcon case editAvatarIcon } public enum AvatarNodeImageOverride { case none case image(TelegramMediaImageRepresentation) case savedMessagesIcon case editAvatarIcon } public enum AvatarNodeColorOverride { case blue } public final class AvatarEditOverlayNode: ASDisplayNode { override public init() { super.init() self.isOpaque = false self.displaysAsynchronously = true } @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() context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.4).cgColor) context.fill(bounds) 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.setBlendMode(.normal) if let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIcon"), color: .white) { context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0), y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0)), size: editAvatarIcon.size)) } } } public final class AvatarNode: ASDisplayNode { var font: UIFont { didSet { if oldValue !== font { if let parameters = self.parameters { self.parameters = AvatarNodeParameters(theme: parameters.theme, accountPeerId: parameters.accountPeerId, peerId: parameters.peerId, letters: parameters.letters, font: self.font, icon: parameters.icon, explicitColorIndex: parameters.explicitColorIndex, hasImage: parameters.hasImage) } if !self.displaySuspended { self.setNeedsDisplay() } } } } private var parameters: AvatarNodeParameters? private var theme: PresentationTheme? let imageNode: ImageNode var editOverlayNode: AvatarEditOverlayNode? private let imageReadyDisposable = MetaDisposable() 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 func didLoad() { super.didLoad() if #available(iOSApplicationExtension 11.0, *), !self.isLayerBacked { self.view.accessibilityIgnoresInvertColors = true } } 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) self.editOverlayNode?.frame = self.imageNode.frame if updateImage && !self.displaySuspended { self.setNeedsDisplay() self.editOverlayNode?.setNeedsDisplay() } } } public func setPeer(account: Account, theme: PresentationTheme, peer: Peer, authorOfMessage: MessageReference? = nil, overrideImage: AvatarNodeImageOverride? = nil, emptyColor: UIColor? = nil, synchronousLoad: Bool = false) { var synchronousLoad = synchronousLoad var representation: TelegramMediaImageRepresentation? var icon = AvatarNodeIcon.none if let overrideImage = overrideImage { switch overrideImage { case .none: representation = nil case let .image(image): representation = image synchronousLoad = false case .savedMessagesIcon: representation = nil icon = .savedMessagesIcon case .editAvatarIcon: representation = peer.smallProfileImage icon = .editAvatarIcon } } else if peer.restrictionText == nil { representation = peer.smallProfileImage } let updatedState: AvatarNodeState = .peerAvatar(peer.id, peer.displayLetters, representation) if updatedState != self.state { self.state = updatedState let parameters: AvatarNodeParameters self.displaySuspended = true self.contents = nil if let signal = peerAvatarImage(account: account, peer: peer, authorOfMessage: authorOfMessage, representation: representation, emptyColor: emptyColor, synchronousLoad: synchronousLoad) { self.imageReady.set(self.imageNode.ready) self.imageNode.setSignal(signal) if case .editAvatarIcon = icon { if self.editOverlayNode == nil { let editOverlayNode = AvatarEditOverlayNode() editOverlayNode.frame = self.imageNode.frame self.addSubnode(editOverlayNode) self.editOverlayNode = editOverlayNode } self.editOverlayNode?.isHidden = false } else { self.editOverlayNode?.isHidden = true } parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true) } else { self.imageReady.set(.single(true)) self.displaySuspended = false if self.isNodeLoaded { self.imageNode.contents = nil } self.editOverlayNode?.isHidden = true parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false) } if self.parameters == nil || self.parameters != parameters { self.parameters = parameters self.setNeedsDisplay() } } } public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil) { var explicitIndex: Int? if let explicitColor = explicitColor { switch explicitColor { case .blue: explicitIndex = 5 } } let updatedState: AvatarNodeState = .custom(letter: letters, explicitColorIndex: explicitIndex) if updatedState != self.state { self.state = updatedState let parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false) self.displaySuspended = true self.contents = nil 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 let explicitColorIndex = parameters.explicitColorIndex { colorIndex = explicitColorIndex } else { if let accountPeerId = parameters.accountPeerId, let peerId = parameters.peerId { if peerId.namespace == -1 { colorIndex = -1 } else { colorIndex = abs(Int(clamping: accountPeerId.id &+ peerId.id)) } } else { colorIndex = -1 } } } else { colorIndex = -1 } let colorsArray: NSArray if let parameters = parameters as? AvatarNodeParameters, parameters.icon != .none { if case .savedMessagesIcon = parameters.icon { colorsArray = savedMessagesColors } else if case .editAvatarIcon = parameters.icon, let theme = parameters.theme { colorsArray = [ theme.list.blocksBackgroundColor.cgColor, theme.list.blocksBackgroundColor.cgColor ] } else { colorsArray = grayscaleColors } } else if colorIndex == -1 { colorsArray = grayscaleColors } else { colorsArray = gradientColors[colorIndex % gradientColors.count] } var locations: [CGFloat] = [1.0, 0.0] 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 { if case .savedMessagesIcon = parameters.icon { let factor = bounds.size.width / 60.0 context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) context.scaleBy(x: factor, y: -factor) context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) if let savedMessagesIcon = savedMessagesIcon { context.draw(savedMessagesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - savedMessagesIcon.size.width) / 2.0), y: floor((bounds.size.height - savedMessagesIcon.size.height) / 2.0)), size: savedMessagesIcon.size)) } } else if case .editAvatarIcon = parameters.icon, let theme = parameters.theme, !parameters.hasImage { 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) if let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIcon"), color: theme.list.freeMonoIcon) { context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0), y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0)), size: editAvatarIcon.size)) } } else { 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 } } } } }