import Foundation import UIKit import Display import AsyncDisplayKit import TelegramCore import Postbox import SwiftSignalKit import TelegramPresentationData import TelegramStringFormatting import PeerOnlineMarkerNode import SelectablePeerNode import ContextUI import AccountContext import TelegramUIPreferences import AnimationCache import MultiAnimationRenderer public enum HorizontalPeerItemMode { case list(compact: Bool) case actionSheet } private let badgeFont = Font.regular(14.0) public final class HorizontalPeerItem: ListViewItem { let theme: PresentationTheme let strings: PresentationStrings let mode: HorizontalPeerItemMode let accountPeerId: EnginePeer.Id let postbox: Postbox let network: Network let energyUsageSettings: EnergyUsageSettings let contentSettings: ContentSettings let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer let resolveInlineStickers: ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> public let peer: EnginePeer let action: (EnginePeer) -> Void let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? let isPeerSelected: (EnginePeer.Id) -> Bool let customWidth: CGFloat? let presence: EnginePeer.Presence? let unreadBadge: (Int32, Bool)? public init( theme: PresentationTheme, strings: PresentationStrings, mode: HorizontalPeerItemMode, accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>, peer: EnginePeer, presence: EnginePeer.Presence?, unreadBadge: (Int32, Bool)?, action: @escaping (EnginePeer) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, isPeerSelected: @escaping (EnginePeer.Id) -> Bool, customWidth: CGFloat? ) { self.theme = theme self.strings = strings self.mode = mode self.accountPeerId = accountPeerId self.postbox = postbox self.network = network self.energyUsageSettings = energyUsageSettings self.contentSettings = contentSettings self.animationCache = animationCache self.animationRenderer = animationRenderer self.resolveInlineStickers = resolveInlineStickers self.peer = peer self.action = action self.contextAction = contextAction self.isPeerSelected = isPeerSelected self.customWidth = customWidth self.presence = presence self.unreadBadge = unreadBadge } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = HorizontalPeerItemNode() let (nodeLayout, apply) = node.asyncLayout()(self, params) node.insets = nodeLayout.insets node.contentSize = nodeLayout.contentSize Queue.mainQueue().async { completion(node, { return (nil, { _ in apply(false, synchronousLoads) }) }) } } } public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { assert(node() is HorizontalPeerItemNode) if let nodeValue = node() as? HorizontalPeerItemNode { let layout = nodeValue.asyncLayout() async { let (nodeLayout, apply) = layout(self, params) Queue.mainQueue().async { completion(nodeLayout, { _ in apply(animation.isAnimated, false) }) } } } } } } public final class HorizontalPeerItemNode: ListViewItemNode { private(set) var peerNode: SelectablePeerNode let badgeBackgroundNode: ASImageNode let badgeTextNode: TextNode let onlineNode: PeerOnlineMarkerNode public private(set) var item: HorizontalPeerItem? public init() { self.peerNode = SelectablePeerNode() self.badgeBackgroundNode = ASImageNode() self.badgeBackgroundNode.isLayerBacked = true self.badgeBackgroundNode.displaysAsynchronously = false self.badgeBackgroundNode.displayWithoutProcessing = true self.badgeTextNode = TextNode() self.badgeTextNode.isUserInteractionEnabled = false self.badgeTextNode.displaysAsynchronously = true self.onlineNode = PeerOnlineMarkerNode() super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.peerNode) self.addSubnode(self.badgeBackgroundNode) self.addSubnode(self.badgeTextNode) self.addSubnode(self.onlineNode) self.peerNode.toggleSelection = { [weak self] _ in if let item = self?.item { item.action(item.peer) } } } deinit { assert(true) } override public func didLoad() { super.didLoad() self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) } public func asyncLayout() -> (HorizontalPeerItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { let badgeTextLayout = TextNode.asyncLayout(self.badgeTextNode) let onlineLayout = self.onlineNode.asyncLayout() let currentItem = self.item return { [weak self] item, params in let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 92.0, height: item.customWidth ?? 80.0), insets: UIEdgeInsets()) let itemTheme: SelectablePeerNodeTheme switch item.mode { case .list: itemTheme = SelectablePeerNodeTheme(textColor: item.theme.list.itemPrimaryTextColor, secretTextColor: item.theme.chatList.secretTitleColor, selectedTextColor: item.theme.list.itemAccentColor, checkBackgroundColor: item.theme.list.plainBackgroundColor, checkFillColor: item.theme.list.itemAccentColor, checkColor: item.theme.list.plainBackgroundColor, avatarPlaceholderColor: item.theme.list.mediaPlaceholderColor) case .actionSheet: itemTheme = SelectablePeerNodeTheme(textColor: item.theme.actionSheet.primaryTextColor, secretTextColor: item.theme.chatList.secretTitleColor, selectedTextColor: item.theme.actionSheet.controlAccentColor, checkBackgroundColor: item.theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: item.theme.actionSheet.controlAccentColor, checkColor: item.theme.actionSheet.opaqueItemBackgroundColor, avatarPlaceholderColor: item.theme.list.mediaPlaceholderColor) } let currentBadgeBackgroundImage: UIImage? let badgeAttributedString: NSAttributedString if let unreadBadge = item.unreadBadge { let badgeTextColor: UIColor let (unreadCount, isMuted) = unreadBadge if isMuted { currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.theme, diameter: 20.0) badgeTextColor = item.theme.chatList.unreadBadgeInactiveTextColor } else { currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.theme, diameter: 20.0) badgeTextColor = item.theme.chatList.unreadBadgeActiveTextColor } badgeAttributedString = NSAttributedString(string: unreadCount > 0 ? "\(unreadCount)" : " ", font: badgeFont, textColor: badgeTextColor) } else { currentBadgeBackgroundImage = nil badgeAttributedString = NSAttributedString() } var online = false let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 if case let .user(peer) = item.peer, let presence = item.presence, !item.peer.isService, !peer.flags.contains(.isSupport) { let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: Int32(timestamp)) if case .online = relativeStatus { online = true } } let (badgeLayout, badgeApply) = badgeTextLayout(TextNodeLayoutArguments(attributedString: badgeAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 50.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var badgeSize: CGFloat = 0.0 if let currentBadgeBackgroundImage = currentBadgeBackgroundImage { badgeSize += max(currentBadgeBackgroundImage.size.width, badgeLayout.size.width + 10.0) + 5.0 } let (onlineLayout, onlineApply) = onlineLayout(online, false) var animateContent = false if let currentItem = currentItem, currentItem.peer.id == item.peer.id { animateContent = true } return (itemLayout, { animated, synchronousLoads in if let strongSelf = self { strongSelf.item = item strongSelf.peerNode.theme = itemTheme if case let .list(compact) = item.mode { strongSelf.peerNode.compact = compact } else { strongSelf.peerNode.compact = false } strongSelf.peerNode.setup(accountPeerId: item.accountPeerId, postbox: item.postbox, network: item.network, energyUsageSettings: item.energyUsageSettings, contentSettings: item.contentSettings, animationCache: item.animationCache, animationRenderer: item.animationRenderer, resolveInlineStickers: item.resolveInlineStickers, theme: item.theme, strings: item.strings, peer: EngineRenderedPeer(peer: item.peer), requiresPremiumForMessaging: false, numberOfLines: 1, synchronousLoad: synchronousLoads) strongSelf.peerNode.frame = CGRect(origin: CGPoint(), size: itemLayout.size) strongSelf.peerNode.updateSelection(selected: item.isPeerSelected(item.peer.id), animated: false) if let contextAction = item.contextAction { strongSelf.peerNode.contextAction = { [weak item] node, gesture, location in if let item = item { contextAction(item.peer, node, gesture, location) } } } else { strongSelf.peerNode.contextAction = nil } let badgeBackgroundWidth: CGFloat if let currentBadgeBackgroundImage = currentBadgeBackgroundImage { strongSelf.badgeBackgroundNode.image = currentBadgeBackgroundImage strongSelf.badgeBackgroundNode.isHidden = false badgeBackgroundWidth = max(badgeLayout.size.width + 10.0, currentBadgeBackgroundImage.size.width) let badgeBackgroundFrame = CGRect(x: itemLayout.size.width - floorToScreenPixels(badgeBackgroundWidth * 1.8), y: 2.0, width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height) let badgeTextFrame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.midX - badgeLayout.size.width / 2.0, y: badgeBackgroundFrame.minY + 2.0), size: badgeLayout.size) strongSelf.badgeTextNode.frame = badgeTextFrame strongSelf.badgeBackgroundNode.frame = badgeBackgroundFrame } else { badgeBackgroundWidth = 0.0 strongSelf.badgeBackgroundNode.image = nil strongSelf.badgeBackgroundNode.isHidden = true } let state: RecentStatusOnlineIconState if case .actionSheet = item.mode { state = .panel } else { state = .regular } strongSelf.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(item.theme, state: state), color: nil, transition: .immediate) strongSelf.onlineNode.frame = CGRect(x: itemLayout.size.width / 2.0 + 14.0, y: itemLayout.size.width - onlineLayout.height - 30.0, width: onlineLayout.width, height: onlineLayout.height) let _ = badgeApply() let _ = onlineApply(animateContent) } }) } } public func updateSelection(animated: Bool) { if let item = self.item { self.peerNode.updateSelection(selected: item.isPeerSelected(item.peer.id), animated: animated) } } override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { super.animateRemoved(currentTimestamp, duration: duration) self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } override public func animateAdded(_ currentTimestamp: Double, duration: Double) { super.animateAdded(currentTimestamp, duration: duration) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } }