mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
286 lines
12 KiB
Swift
286 lines
12 KiB
Swift
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<Empty>?
|
|
public var storyData: (hasUnseen: Bool, hasUnseenCloseFriends: Bool)?
|
|
|
|
public let statusView: ComponentView<Empty>
|
|
|
|
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<Empty>
|
|
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
|
|
}
|
|
}
|
|
}
|