// // StatusTableViewCell.swift // Tusker // // Created by Shadowfacts on 8/16/18. // Copyright © 2018 Shadowfacts. All rights reserved. // import UIKit import MastodonKit import SwiftSoup class StatusTableViewCell: UITableViewCell { @IBOutlet weak var displayNameLabel: UILabel! @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var contentLabel: UILabel! @IBOutlet weak var avatarImageView: UIImageView! var status: Status! var avatarURL: URL? var layoutManager: NSLayoutManager! var textContainer: NSTextContainer! var textStorage: NSTextStorage! var links: [NSRange: URL] = [:] func updateUI(for status: Status) { self.status = status let account: Account if let reblog = status.reblog { account = reblog.account } else { account = status.account } displayNameLabel.text = account.displayName usernameLabel.text = "@\(account.acct)" avatarImageView.layer.cornerRadius = 5 avatarImageView.layer.masksToBounds = true avatarImageView.image = nil if let url = URL(string: account.avatar) { AvatarCache.shared.get(url) { image in DispatchQueue.main.async { self.avatarImageView.image = image } } } let doc = try! SwiftSoup.parse(status.content) let body = doc.body()! // print("---") // print(status.content) // print("---") let (text, links) = attributedTextForNode(body) self.links = links contentLabel.attributedText = text contentLabel.isUserInteractionEnabled = true contentLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTapOnContentLabel(_:)))) // https://stackoverflow.com/a/28519273/4731558 layoutManager = NSLayoutManager() textContainer = NSTextContainer(size: .zero) textStorage = NSTextStorage(attributedString: text) layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) textContainer.lineFragmentPadding = 0 textContainer.lineBreakMode = contentLabel.lineBreakMode textContainer.maximumNumberOfLines = contentLabel.numberOfLines } func attributedTextForNode(_ node: Node) -> (NSAttributedString, [NSRange: URL]) { switch node { case let node as TextNode: return (NSAttributedString(string: node.text()), [:]) case let node as Element: var links = [NSRange: URL]() let attributed = NSMutableAttributedString() node.getChildNodes().forEach { child in let (text, childLinks) = attributedTextForNode(child) childLinks.forEach { range, url in let newRange = NSRange(location: range.location + attributed.length, length: range.length) links[newRange] = url } attributed.append(text) } switch node.tagName() { case "br": attributed.append(NSAttributedString(string: "\n")) case "a": if let link = try? node.attr("href"), let url = URL(string: link) { let linkRange = NSRange(location: 0, length: attributed.length) let linkAttributes: [NSAttributedString.Key: Any] = [ .foregroundColor: UIColor.blue ] attributed.setAttributes(linkAttributes, range: linkRange) links[linkRange] = url } default: break } return (attributed, links) default: fatalError("Unexpected node type: \(type(of: node))") } } @objc func handleTapOnContentLabel(_ tapGesture: UITapGestureRecognizer) { guard let view = tapGesture.view else { fatalError() } let locationOfTouchInLabel = tapGesture.location(in: view) let labelSize = view.bounds.size let textBoundingBox = layoutManager.usedRect(for: textContainer) let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y) let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y) let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) let link = links.first { (range, _) -> Bool in range.contains(indexOfCharacter) } if let (_, url) = link { print("Open URL: \(url)") } } override func prepareForReuse() { if let url = avatarURL { AvatarCache.shared.cancel(url) } } }