import Foundation import UIKit import AsyncDisplayKit import Postbox import Display import SwiftSignalKit import TelegramCore import TelegramPresentationData import ItemListUI import LocationResources import AppBundle import SolidRoundedButtonNode final class LocationInfoListItem: ListViewItem { let presentationData: ItemListPresentationData let engine: TelegramEngine let location: TelegramMediaMap let address: String? let distance: String? let eta: String? let action: () -> Void let getDirections: () -> Void public init(presentationData: ItemListPresentationData, engine: TelegramEngine, location: TelegramMediaMap, address: String?, distance: String?, eta: String?, action: @escaping () -> Void, getDirections: @escaping () -> Void) { self.presentationData = presentationData self.engine = engine self.location = location self.address = address self.distance = distance self.eta = eta self.action = action self.getDirections = getDirections } 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 = LocationInfoListItemNode() let makeLayout = node.asyncLayout() let (nodeLayout, nodeApply) = makeLayout(self, params) node.contentSize = nodeLayout.contentSize node.insets = nodeLayout.insets completion(node, nodeApply) } } 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 { if let nodeValue = node() as? LocationInfoListItemNode { let layout = nodeValue.asyncLayout() async { let (nodeLayout, apply) = layout(self, params) Queue.mainQueue().async { completion(nodeLayout, { info in apply().1(info) }) } } } } } public var selectable: Bool { return false } } final class LocationInfoListItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private var titleNode: TextNode? private var subtitleNode: TextNode? private let venueIconNode: TransformImageNode private let buttonNode: HighlightableButtonNode private var directionsButtonNode: SolidRoundedButtonNode? private var item: LocationInfoListItem? private var layoutParams: ListViewItemLayoutParams? required init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.buttonNode = HighlightableButtonNode() self.venueIconNode = TransformImageNode() self.venueIconNode.isUserInteractionEnabled = false super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) self.addSubnode(self.backgroundNode) self.addSubnode(self.buttonNode) self.addSubnode(self.venueIconNode) self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.titleNode?.layer.removeAnimation(forKey: "opacity") strongSelf.titleNode?.alpha = 0.4 strongSelf.subtitleNode?.layer.removeAnimation(forKey: "opacity") strongSelf.subtitleNode?.alpha = 0.4 strongSelf.venueIconNode.layer.removeAnimation(forKey: "opacity") strongSelf.venueIconNode.alpha = 0.4 } else { strongSelf.titleNode?.alpha = 1.0 strongSelf.titleNode?.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.subtitleNode?.alpha = 1.0 strongSelf.subtitleNode?.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.venueIconNode.alpha = 1.0 strongSelf.venueIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = self.item { let makeLayout = self.asyncLayout() let (nodeLayout, nodeApply) = makeLayout(item, params) self.contentSize = nodeLayout.contentSize self.insets = nodeLayout.insets let _ = nodeApply() } } func asyncLayout() -> (_ item: LocationInfoListItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal?, (ListViewItemApply) -> Void)) { let currentItem = self.item let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) let iconLayout = self.venueIconNode.asyncLayout() return { [weak self] item, params in let leftInset: CGFloat = 75.0 + params.leftInset let rightInset: CGFloat = params.rightInset let verticalInset: CGFloat = 14.0 let iconSize: CGFloat = 48.0 let inset: CGFloat = 15.0 let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) let title: String let subtitle: String var subtitleComponents: [String] = [] if let venue = item.location.venue { title = venue.title } else { title = item.presentationData.strings.Map_Location } if let address = item.address { subtitleComponents.append(address) } if let distance = item.distance { subtitleComponents.append(distance) } subtitle = subtitleComponents.joined(separator: " • ") let titleAttributedString = NSAttributedString(string: title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let subtitleAttributedString = NSAttributedString(string: subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let titleSpacing: CGFloat = 1.0 let bottomInset: CGFloat = 4.0 let contentSize = CGSize(width: params.width, height: max(126.0, verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + bottomInset)) let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets()) return (nodeLayout, { [weak self] in var updatedTheme: PresentationTheme? if currentItem?.presentationData.theme !== item.presentationData.theme { updatedTheme = item.presentationData.theme } var updatedLocation: TelegramMediaMap? if currentItem?.location.venue?.id != item.location.venue?.id || updatedTheme != nil { updatedLocation = item.location } return (nil, { _ in if let strongSelf = self { strongSelf.item = item strongSelf.layoutParams = params if let _ = updatedTheme { strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor strongSelf.directionsButtonNode?.updateTheme(SolidRoundedButtonTheme(theme: item.presentationData.theme)) } let arguments = VenueIconArguments(defaultBackgroundColor: item.presentationData.theme.chat.inputPanel.actionControlFillColor, defaultForegroundColor: item.presentationData.theme.chat.inputPanel.actionControlForegroundColor) if let updatedLocation = updatedLocation { strongSelf.venueIconNode.setSignal(venueIcon(engine: item.engine, type: updatedLocation.venue?.type ?? "", background: true)) } let iconApply = iconLayout(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: iconSize, height: iconSize), boundingSize: CGSize(width: iconSize, height: iconSize), intrinsicInsets: UIEdgeInsets(), custom: arguments)) iconApply() let titleNode = titleApply() if strongSelf.titleNode == nil { titleNode.isUserInteractionEnabled = false strongSelf.titleNode = titleNode strongSelf.addSubnode(titleNode) } let subtitleNode = subtitleApply() if strongSelf.subtitleNode == nil { subtitleNode.isUserInteractionEnabled = false strongSelf.subtitleNode = subtitleNode strongSelf.addSubnode(subtitleNode) } let directionsButtonNode: SolidRoundedButtonNode if let currentDirectionsButtonNode = strongSelf.directionsButtonNode { directionsButtonNode = currentDirectionsButtonNode } else { directionsButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: item.presentationData.theme), height: 50.0, cornerRadius: 10.0) directionsButtonNode.title = item.presentationData.strings.Map_Directions directionsButtonNode.pressed = { item.getDirections() } strongSelf.addSubnode(directionsButtonNode) strongSelf.directionsButtonNode = directionsButtonNode } directionsButtonNode.subtitle = item.eta let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) titleNode.frame = titleFrame let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size) subtitleNode.frame = subtitleFrame let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + inset, y: 10.0), size: CGSize(width: iconSize, height: iconSize)) strongSelf.venueIconNode.frame = iconNodeFrame let directionsWidth = contentSize.width - inset * 2.0 let directionsHeight = directionsButtonNode.updateLayout(width: directionsWidth, transition: .immediate) directionsButtonNode.frame = CGRect(x: inset, y: iconNodeFrame.maxY + 14.0, width: directionsWidth, height: directionsHeight) strongSelf.buttonNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: 72.0) strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height)) } }) }) } } override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) } @objc private func buttonPressed() { self.item?.action() } }