import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import Postbox import TelegramCore import AvatarNode import ContextUI import TelegramPresentationData import TelegramUniversalVideoContent import UniversalMediaPlayer import GalleryUI import HierarchyTrackingLayer import AccountContext import ComponentFlow import EmojiStatusComponent import AvatarVideoNode import AvatarStoryIndicatorComponent import ComponentDisplayAdapters private let normalFont = avatarPlaceholderFont(size: 16.0) private let smallFont = avatarPlaceholderFont(size: 12.0) public final class ChatAvatarNavigationNode: ASDisplayNode { private var context: AccountContext? private let containerNode: ContextControllerSourceNode public let avatarNode: AvatarNode private var avatarVideoNode: AvatarVideoNode? public private(set) var avatarStoryView: ComponentView? public var storyData: (hasUnseen: Bool, hasUnseenCloseFriends: Bool)? public let statusView: ComponentView private var cachedDataDisposable = MetaDisposable() private var hierarchyTrackingLayer: HierarchyTrackingLayer? private var trackingIsInHierarchy: Bool = false { didSet { if self.trackingIsInHierarchy != oldValue { Queue.mainQueue().justDispatch { self.updateVideoVisibility() } } } } public var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? public var contextActionIsEnabled: Bool = false { didSet { if self.contextActionIsEnabled != oldValue { self.containerNode.isGestureEnabled = self.contextActionIsEnabled } } } override public init() { self.containerNode = ContextControllerSourceNode() self.containerNode.isGestureEnabled = false self.avatarNode = AvatarNode(font: normalFont) self.statusView = ComponentView() super.init() self.addSubnode(self.containerNode) self.containerNode.addSubnode(self.avatarNode) self.containerNode.activated = { [weak self] gesture, _ in guard let strongSelf = self else { return } strongSelf.contextAction?(strongSelf.containerNode, gesture) } self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0)).offsetBy(dx: 10.0, dy: 1.0) self.avatarNode.frame = self.containerNode.bounds #if DEBUG //self.hasUnseenStories = true #endif } deinit { self.cachedDataDisposable.dispose() } override public func didLoad() { super.didLoad() self.view.isOpaque = false } public func setStatus(context: AccountContext, content: EmojiStatusComponent.Content) { let statusSize = self.statusView.update( transition: .immediate, component: AnyComponent(EmojiStatusComponent( context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, content: content, isVisibleForAnimations: true, action: nil )), environment: {}, containerSize: CGSize(width: 32.0, height: 32.0) ) if let statusComponentView = self.statusView.view { if statusComponentView.superview == nil { self.containerNode.view.addSubview(statusComponentView) } statusComponentView.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - statusSize.width) / 2.0), y: floor((self.containerNode.bounds.height - statusSize.height) / 2.0)), size: statusSize) } self.avatarNode.isHidden = true } public func setPeer(context: AccountContext, theme: PresentationTheme, peer: EnginePeer?, authorOfMessage: MessageReference? = nil, overrideImage: AvatarNodeImageOverride? = nil, emptyColor: UIColor? = nil, clipStyle: AvatarNodeClipStyle = .round, synchronousLoad: Bool = false, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), storeUnrounded: Bool = false) { self.context = context self.avatarNode.setPeer(context: context, theme: theme, peer: peer, authorOfMessage: authorOfMessage, overrideImage: overrideImage, emptyColor: emptyColor, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: displayDimensions, storeUnrounded: storeUnrounded) if let peer = peer, peer.isPremium { self.cachedDataDisposable.set((context.account.postbox.peerView(id: peer.id) |> deliverOnMainQueue).start(next: { [weak self] peerView in guard let strongSelf = self else { return } let cachedPeerData = peerView.cachedData as? CachedUserData var personalPhoto: TelegramMediaImage? var profilePhoto: TelegramMediaImage? var isKnown = false if let cachedPeerData = cachedPeerData { if case let .known(maybePersonalPhoto) = cachedPeerData.personalPhoto { personalPhoto = maybePersonalPhoto isKnown = true } if case let .known(maybePhoto) = cachedPeerData.photo { profilePhoto = maybePhoto isKnown = true } } if isKnown { let photo = personalPhoto ?? profilePhoto if let photo = photo, !photo.videoRepresentations.isEmpty || photo.emojiMarkup != nil { let videoNode: AvatarVideoNode if let current = strongSelf.avatarVideoNode { videoNode = current } else { videoNode = AvatarVideoNode(context: context) strongSelf.avatarNode.addSubnode(videoNode) strongSelf.avatarVideoNode = videoNode } videoNode.update(peer: peer, photo: photo, size: CGSize(width: 37.0, height: 37.0)) if strongSelf.hierarchyTrackingLayer == nil { let hierarchyTrackingLayer = HierarchyTrackingLayer() hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in guard let strongSelf = self else { return } strongSelf.trackingIsInHierarchy = true } hierarchyTrackingLayer.didExitHierarchy = { [weak self] in guard let strongSelf = self else { return } strongSelf.trackingIsInHierarchy = false } strongSelf.hierarchyTrackingLayer = hierarchyTrackingLayer strongSelf.layer.addSublayer(hierarchyTrackingLayer) } } else { if let avatarVideoNode = strongSelf.avatarVideoNode { avatarVideoNode.removeFromSupernode() strongSelf.avatarVideoNode = nil } strongSelf.hierarchyTrackingLayer?.removeFromSuperlayer() strongSelf.hierarchyTrackingLayer = nil } strongSelf.updateVideoVisibility() } else { if let photo = peer.largeProfileImage, photo.hasVideo { let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start() } } })) } else { self.cachedDataDisposable.set(nil) self.avatarVideoNode?.removeFromSupernode() self.avatarVideoNode = nil self.hierarchyTrackingLayer?.removeFromSuperlayer() self.hierarchyTrackingLayer = nil } } public func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme) { if let storyData = self.storyData { let avatarStoryView: ComponentView if let current = self.avatarStoryView { avatarStoryView = current } else { avatarStoryView = ComponentView() self.avatarStoryView = avatarStoryView } let _ = avatarStoryView.update( transition: Transition(transition), component: AnyComponent(AvatarStoryIndicatorComponent( hasUnseen: storyData.hasUnseen, hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends, colors: AvatarStoryIndicatorComponent.Colors(theme: theme), activeLineWidth: 1.0, inactiveLineWidth: 1.0, counters: nil )), environment: {}, containerSize: self.avatarNode.bounds.insetBy(dx: 2.0, dy: 2.0).size ) if let avatarStoryComponentView = avatarStoryView.view { if avatarStoryComponentView.superview == nil { self.containerNode.view.insertSubview(avatarStoryComponentView, at: 0) } avatarStoryComponentView.frame = self.avatarNode.frame } } else { if let avatarStoryView = self.avatarStoryView { self.avatarStoryView = nil avatarStoryView.view?.removeFromSuperview() } } } override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { return CGSize(width: 37.0, height: 37.0) } public func onLayout() { } public final class SnapshotState { fileprivate let snapshotView: UIView? fileprivate init(snapshotView: UIView?) { self.snapshotView = snapshotView } } public func prepareSnapshotState() -> SnapshotState { let snapshotView = self.avatarNode.view.snapshotView(afterScreenUpdates: false) return SnapshotState( snapshotView: snapshotView ) } public func animateFromSnapshot(_ snapshotState: SnapshotState) { self.avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.avatarNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true) if let snapshotView = snapshotState.snapshotView { snapshotView.frame = self.frame self.containerNode.view.addSubview(snapshotView) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) } } private func updateVideoVisibility() { let isVisible = self.trackingIsInHierarchy self.avatarVideoNode?.updateVisibility(isVisible) if let videoNode = self.avatarVideoNode { videoNode.updateLayout(size: self.avatarNode.frame.size, cornerRadius: self.avatarNode.frame.size.width / 2.0, transition: .immediate) videoNode.frame = self.avatarNode.bounds } } }