import Foundation import AsyncDisplayKit import Display import Postbox import TelegramCore import SwiftSignalKit private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:]) private let redColors: (UInt32, UInt32) = (0xf0625d, 0xde524e) private let greenColors: (UInt32, UInt32) = (0x72ce76, 0x54b658) private let blueColors: (UInt32, UInt32) = (0x60b0e8, 0x4597d1) private let yellowColors: (UInt32, UInt32) = (0xf5c565, 0xe5a64e) private let extensionColorsMap: [String: (UInt32, UInt32)] = [ "ppt": redColors, "pptx": redColors, "pdf": redColors, "key": redColors, "xls": greenColors, "xlsx": greenColors, "csv": greenColors, "zip": yellowColors, "rar": yellowColors, "gzip": yellowColors, "ai": yellowColors ] private func generateExtensionImage(colors: (UInt32, UInt32)) -> UIImage? { return generateImage(CGSize(width: 42.0, height: 42.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -size.width / 2.0 + 1.0, y: -size.height / 2.0 + 1.0) let radius: CGFloat = 2.0 let cornerSize: CGFloat = 10.0 let size = CGSize(width: 42.0, height: 42.0) context.setFillColor(UIColor(colors.0).cgColor) context.beginPath() context.move(to: CGPoint(x: 0.0, y: radius)) if !radius.isZero { context.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: radius, y: 0.0), radius: radius) } context.addLine(to: CGPoint(x: size.width - cornerSize, y: 0.0)) context.addLine(to: CGPoint(x: size.width - cornerSize + cornerSize / 4.0, y: cornerSize - cornerSize / 4.0)) context.addLine(to: CGPoint(x: size.width, y: cornerSize)) context.addLine(to: CGPoint(x: size.width, y: size.height - radius)) if !radius.isZero { context.addArc(tangent1End: CGPoint(x: size.width, y: size.height), tangent2End: CGPoint(x: size.width - radius, y: size.height), radius: radius) } context.addLine(to: CGPoint(x: radius, y: size.height)) if !radius.isZero { context.addArc(tangent1End: CGPoint(x: 0.0, y: size.height), tangent2End: CGPoint(x: 0.0, y: size.height - radius), radius: radius) } context.closePath() context.fillPath() context.setFillColor(UIColor(colors.1).cgColor) context.beginPath() context.move(to: CGPoint(x: size.width - cornerSize, y: 0.0)) context.addLine(to: CGPoint(x: size.width, y: cornerSize)) context.addLine(to: CGPoint(x: size.width - cornerSize + radius, y: cornerSize)) if !radius.isZero { context.addArc(tangent1End: CGPoint(x: size.width - cornerSize, y: cornerSize), tangent2End: CGPoint(x: size.width - cornerSize, y: cornerSize - radius), radius: radius) } context.closePath() context.fillPath() }) } private func extensionImage(fileExtension: String?) -> UIImage? { let colors: (UInt32, UInt32) if let fileExtension = fileExtension { if let extensionColors = extensionColorsMap[fileExtension] { colors = extensionColors } else { colors = blueColors } } else { colors = blueColors } if let cachedImage = (extensionImageCache.with { dict in return dict[colors.0] }) { return cachedImage } else if let image = generateExtensionImage(colors: colors) { let _ = extensionImageCache.modify { dict in var dict = dict dict[colors.0] = image return dict } return image } else { return nil } } private let titleFont = Font.medium(16.0) private let descriptionFont = Font.regular(13.0) private let extensionFont = Font.medium(13.0) private let downloadFileStartIcon = generateTintedImage(image: UIImage(bundleImageName: "List Menu/ListDownloadStartIcon"), color: UIColor(0x1195f2)) private let downloadFilePauseIcon = generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor(0x1195f2).cgColor) context.fill(CGRect(x: 2.0, y: 0.0, width: 2.0, height: 11.0 - 1.0)) context.fill(CGRect(x: 2.0 + 2.0 + 2.0, y: 0.0, width: 2.0, height: 11.0 - 1.0)) }) private struct FetchControls { let fetch: () -> Void let cancel: () -> Void } final class ListMessageFileItemNode: ListMessageNode { private let highlightedBackgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode private let titleNode: TextNode private let descriptionNode: TextNode private let extensionIconNode: ASImageNode private let extensionIconText: TextNode private let iconImageNode: TransformImageNode private var currentIconImageRepresentation: TelegramMediaImageRepresentation? private var currentMedia: Media? private let statusDisposable = MetaDisposable() private let fetchControls = Atomic(value: nil) private var fetchStatus: MediaResourceStatus? private let fetchDisposable = MetaDisposable() private var downloadStatusIconNode: ASImageNode private var progressNode: ASDisplayNode public required init() { self.separatorNode = ASDisplayNode() self.separatorNode.backgroundColor = UIColor(0xc8c7cc) self.separatorNode.displaysAsynchronously = false self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true self.titleNode = TextNode() self.titleNode.isLayerBacked = true self.descriptionNode = TextNode() self.descriptionNode.isLayerBacked = true self.extensionIconNode = ASImageNode() self.extensionIconNode.isLayerBacked = true self.extensionIconNode.displaysAsynchronously = false self.extensionIconNode.displayWithoutProcessing = true self.extensionIconText = TextNode() self.extensionIconText.isLayerBacked = true self.iconImageNode = TransformImageNode() self.iconImageNode.displaysAsynchronously = false self.downloadStatusIconNode = ASImageNode() self.downloadStatusIconNode.isLayerBacked = true self.downloadStatusIconNode.displaysAsynchronously = false self.downloadStatusIconNode.displayWithoutProcessing = true self.progressNode = ASDisplayNode() self.progressNode.backgroundColor = UIColor(0x1195f2) self.progressNode.isLayerBacked = true super.init() self.addSubnode(self.separatorNode) self.addSubnode(self.titleNode) self.addSubnode(self.descriptionNode) self.addSubnode(self.extensionIconNode) self.addSubnode(self.extensionIconText) } deinit { self.statusDisposable.dispose() self.fetchDisposable.dispose() } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func setupItem(_ item: ListMessageItem) { self.item = item } override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ListMessageItem { let doLayout = self.asyncLayout() let merged = (top: false, bottom: false)//item.mergedWithItems(top: previousItem, bottom: nextItem) let (layout, apply) = doLayout(item, width, merged.top, merged.bottom) self.contentSize = layout.contentSize self.insets = layout.insets apply(.None) } } override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { super.animateInsertion(currentTimestamp, duration: duration) self.transitionOffset = self.bounds.size.height * 1.6 self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp) //self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height * 1.4, to: 0.0, duration: duration) } override func asyncLayout() -> (_ item: ListMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let titleNodeMakeLayout = TextNode.asyncLayout(self.titleNode) let descriptionNodeMakeLayout = TextNode.asyncLayout(self.descriptionNode) let extensionIconTextMakeLayout = TextNode.asyncLayout(self.extensionIconText) let iconImageLayout = self.iconImageNode.asyncLayout() let currentMedia = self.currentMedia let currentIconImageRepresentation = self.currentIconImageRepresentation return { [weak self] item, width, _, _ in let leftInset: CGFloat = 65.0 var extensionIconImage: UIImage? var title: NSAttributedString? var descriptionText: NSAttributedString? var extensionText: NSAttributedString? var iconImageRepresentation: TelegramMediaImageRepresentation? var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext, NoError>? var updatedStatusSignal: Signal? var updatedFetchControls: FetchControls? var selectedMedia: TelegramMediaFile? for media in item.message.media { if let file = media as? TelegramMediaFile { selectedMedia = file let fileName: String = file.fileName ?? "" title = NSAttributedString(string: fileName, font: titleFont, textColor: UIColor.black) var fileExtension: String? if let range = fileName.range(of: ".", options: [.backwards]) { fileExtension = fileName.substring(from: range.upperBound).lowercased() } extensionIconImage = extensionImage(fileExtension: fileExtension) if let fileExtension = fileExtension { extensionText = NSAttributedString(string: fileExtension, font: extensionFont, textColor: UIColor.white) } iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) let dateFormatter = DateFormatter() dateFormatter.dateFormat = "MMM d, yyyy 'at' h a" let dateString = dateFormatter.string(from: Date(timeIntervalSince1970: Double(item.message.timestamp))) descriptionText = NSAttributedString(string: "\(dataSizeString(file.size)) • \(dateString)", font: descriptionFont, textColor: UIColor(0xa8a8a8)) break } } var mediaUpdated = false if let currentMedia = currentMedia { if let selectedMedia = selectedMedia { mediaUpdated = !selectedMedia.isEqual(currentMedia) } else { mediaUpdated = true } } else { mediaUpdated = selectedMedia != nil } if let selectedMedia = selectedMedia, mediaUpdated { let account = item.account updatedStatusSignal = chatMessageFileStatus(account: account, file: selectedMedia) updatedFetchControls = FetchControls(fetch: { [weak self] in if let strongSelf = self { strongSelf.fetchDisposable.set(chatMessageFileInteractiveFetched(account: account, file: selectedMedia).start()) } }, cancel: { chatMessageFileCancelInteractiveFetch(account: account, file: selectedMedia) }) } let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(title, nil, 1, .middle, CGSize(width: width - leftInset - 8.0, height: CGFloat.infinity), nil) let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(descriptionText, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - 12.0, height: CGFloat.infinity), nil) let (extensionTextLayout, extensionTextApply) = extensionIconTextMakeLayout(extensionText, nil, 1, .end, CGSize(width: 38.0, height: CGFloat.infinity), nil) var iconImageApply: (() -> Void)? if let iconImageRepresentation = iconImageRepresentation { let iconSize = CGSize(width: 42.0, height: 42.0) let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageRepresentation.dimensions.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets()) iconImageApply = iconImageLayout(arguments) } if currentIconImageRepresentation != iconImageRepresentation { if let iconImageRepresentation = iconImageRepresentation { let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation]) updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: tmpImage) } else { updateIconImageSignal = .complete() } } return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 52.0), insets: UIEdgeInsets()), { _ in if let strongSelf = self { strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 52.0 - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 52.0 + UIScreenPixel)) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 9.0), size: titleNodeLayout.size) let _ = titleNodeApply() var descriptionOffset: CGFloat = 0.0 if let fetchStatus = strongSelf.fetchStatus { switch fetchStatus { case .Remote, .Fetching: descriptionOffset = 14.0 case .Local: break } } strongSelf.descriptionNode.frame = CGRect(origin: CGPoint(x: leftInset + descriptionOffset, y: 29.0), size: descriptionNodeLayout.size) let _ = descriptionNodeApply() let iconFrame = CGRect(origin: CGPoint(x: 9.0, y: 5.0), size: CGSize(width: 42.0, height: 42.0)) strongSelf.extensionIconNode.frame = iconFrame strongSelf.extensionIconNode.image = extensionIconImage strongSelf.extensionIconText.frame = CGRect(origin: CGPoint(x: 9.0 + floor((42.0 - extensionTextLayout.size.width) / 2.0), y: 5.0 + floor((42.0 - extensionTextLayout.size.height) / 2.0)), size: extensionTextLayout.size) let _ = extensionTextApply() strongSelf.currentIconImageRepresentation = iconImageRepresentation if let iconImageApply = iconImageApply { if let updateImageSignal = updateIconImageSignal { strongSelf.iconImageNode.setSignal(account: item.account, signal: updateImageSignal) } strongSelf.iconImageNode.frame = iconFrame if strongSelf.iconImageNode.supernode == nil { strongSelf.addSubnode(strongSelf.iconImageNode) } iconImageApply() if strongSelf.extensionIconNode.supernode != nil { strongSelf.extensionIconNode.removeFromSupernode() } if strongSelf.extensionIconText.supernode != nil { strongSelf.extensionIconText.removeFromSupernode() } } else if strongSelf.iconImageNode.supernode != nil { strongSelf.iconImageNode.removeFromSupernode() if strongSelf.extensionIconNode.supernode == nil { strongSelf.addSubnode(strongSelf.extensionIconNode) } if strongSelf.extensionIconText.supernode == nil { strongSelf.addSubnode(strongSelf.extensionIconText) } } if let updatedStatusSignal = updatedStatusSignal { strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in displayLinkDispatcher.dispatch { if let strongSelf = strongSelf { strongSelf.fetchStatus = status strongSelf.updateProgressFrame(size: strongSelf.bounds.size) } } })) } strongSelf.updateProgressFrame(size: CGSize(width: width, height: 52.0)) strongSelf.downloadStatusIconNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 32.0), size: CGSize(width: 11.0, height: 11.0)) if let updatedFetchControls = updatedFetchControls { let _ = strongSelf.fetchControls.swap(updatedFetchControls) } } }) } } override func setHighlighted(_ highlighted: Bool, animated: Bool) { super.setHighlighted(highlighted, animated: animated) if highlighted { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) } } else { if self.highlightedBackgroundNode.supernode != nil { if animated { self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in if let strongSelf = self { if completed { strongSelf.highlightedBackgroundNode.removeFromSupernode() } } }) self.highlightedBackgroundNode.alpha = 0.0 } else { self.highlightedBackgroundNode.removeFromSupernode() } } } } override func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? { if let item = self.item, item.message.id == id, self.iconImageNode.supernode != nil { return self.iconImageNode } return nil } override func updateHiddenMedia() { if let controllerInteraction = self.controllerInteraction, let item = self.item, controllerInteraction.hiddenMedia[item.message.id] != nil { self.iconImageNode.isHidden = true } else { self.iconImageNode.isHidden = false } } override func updateSelectionState(animated: Bool) { } private func updateProgressFrame(size: CGSize) { var descriptionOffset: CGFloat = 0.0 if let fetchStatus = self.fetchStatus { switch fetchStatus { case .Remote, .Fetching: descriptionOffset = 14.0 case .Local: break } switch fetchStatus { case let .Fetching(progress): let progressFrame = CGRect(x: 65.0, y: size.height - 2.0, width: floor((size.width - 65.0) * CGFloat(progress)), height: 2.0) if self.progressNode.supernode == nil { self.addSubnode(self.progressNode) } if !self.progressNode.frame.equalTo(progressFrame) { self.progressNode.frame = progressFrame } if self.downloadStatusIconNode.supernode == nil { self.addSubnode(self.downloadStatusIconNode) } self.downloadStatusIconNode.image = downloadFilePauseIcon case .Local: if self.progressNode.supernode != nil { self.progressNode.removeFromSupernode() } if self.downloadStatusIconNode.supernode != nil { self.downloadStatusIconNode.removeFromSupernode() } self.downloadStatusIconNode.image = nil case .Remote: if self.progressNode.supernode != nil { self.progressNode.removeFromSupernode() } if self.downloadStatusIconNode.supernode == nil { self.addSubnode(self.downloadStatusIconNode) } self.downloadStatusIconNode.image = downloadFileStartIcon } } else { if self.progressNode.supernode != nil { self.progressNode.removeFromSupernode() } if self.downloadStatusIconNode.supernode != nil { self.downloadStatusIconNode.removeFromSupernode() } } var descriptionFrame = self.descriptionNode.frame if !descriptionFrame.origin.x.isEqual(to: 65.0 + descriptionOffset) { descriptionFrame.origin.x = 65.0 + descriptionOffset self.descriptionNode.frame = descriptionFrame } } func activateMedia() { if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { if let item = self.item, let controllerInteraction = self.controllerInteraction { controllerInteraction.openMessage(item.message.id) } } else { self.progressPressed() } } func progressPressed() { if let fetchStatus = self.fetchStatus { switch fetchStatus { case .Fetching: if let cancel = self.fetchControls.with({ return $0?.cancel }) { cancel() } case .Remote: if let fetch = self.fetchControls.with({ return $0?.fetch }) { fetch() } case .Local: break } } } }