import Foundation import UIKit import Display import Postbox import AsyncDisplayKit import TelegramCore import SyncCore import SwiftSignalKit import TelegramPresentationData import TelegramStringFormatting import PeerOnlineMarkerNode import SelectablePeerNode import ContextUI import AccountContext public enum HorizontalPeerItemMode { case list case actionSheet } private let badgeFont = Font.regular(14.0) public final class HorizontalPeerItem: ListViewItem { let theme: PresentationTheme let strings: PresentationStrings let mode: HorizontalPeerItemMode let context: AccountContext public let peer: Peer let action: (Peer) -> Void let contextAction: (Peer, ASDisplayNode, ContextGesture?) -> Void let isPeerSelected: (PeerId) -> Bool let customWidth: CGFloat? let presence: PeerPresence? let unreadBadge: (Int32, Bool)? public init(theme: PresentationTheme, strings: PresentationStrings, mode: HorizontalPeerItemMode, context: AccountContext, peer: Peer, presence: PeerPresence?, unreadBadge: (Int32, Bool)?, action: @escaping (Peer) -> Void, contextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, customWidth: CGFloat?) { self.theme = theme self.strings = strings self.mode = mode self.context = context 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) } } self.peerNode.contextAction = { [weak self] node, gesture in if let item = self?.item { item.contextAction(item.peer, node, gesture) } } } 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 let peer = item.peer as? TelegramUser, let presence = item.presence as? TelegramUserPresence, !isServicePeer(peer) && !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 strongSelf.peerNode.setup(context: item.context, theme: item.theme, strings: item.strings, peer: RenderedPeer(peer: item.peer), numberOfLines: 1, synchronousLoad: synchronousLoads) strongSelf.peerNode.frame = CGRect(origin: CGPoint(), size: itemLayout.size) strongSelf.peerNode.updateSelection(selected: item.isPeerSelected(item.peer.id), animated: false) 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 } strongSelf.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(item.theme, state: .regular), color: nil, transition: .immediate) strongSelf.onlineNode.frame = CGRect(x: itemLayout.size.width - onlineLayout.width - 18.0, y: itemLayout.size.height - onlineLayout.height - 18.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, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) 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) } }