195 lines
7.2 KiB
Swift
195 lines
7.2 KiB
Swift
//
|
|
// ContentLabel.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 10/1/18.
|
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import SafariServices
|
|
import Pachyderm
|
|
import SwiftSoup
|
|
|
|
class ContentLabel: LinkLabel {
|
|
|
|
private static let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
|
|
|
var navigationDelegate: TuskerNavigationDelegate?
|
|
|
|
// MARK: - Emojis
|
|
func setEmojis(_ emojis: [Emoji]) {
|
|
guard !emojis.isEmpty else { return }
|
|
|
|
let group = DispatchGroup()
|
|
|
|
let mutAttrString = NSMutableAttributedString(attributedString: self.attributedText!)
|
|
let string = mutAttrString.string
|
|
let matches = ContentLabel.emojiRegex.matches(in: string, options: [], range: NSRange(location: 0, length: mutAttrString.length))
|
|
for match in matches.reversed() {
|
|
let shortcode = (string as NSString).substring(with: match.range(at: 1))
|
|
guard let emoji = emojis.first(where: { $0.shortcode == shortcode }) else {
|
|
continue
|
|
}
|
|
|
|
group.enter()
|
|
ImageCache.emojis.get(emoji.url) { (data) in
|
|
guard let data = data, let image = UIImage(data: data) else {
|
|
group.leave()
|
|
return
|
|
}
|
|
DispatchQueue.main.async {
|
|
let attachment = self.createEmojiTextAttachment(image: image, index: match.range.location)
|
|
mutAttrString.replaceCharacters(in: match.range, with: NSAttributedString(attachment: attachment))
|
|
|
|
group.leave()
|
|
}
|
|
}
|
|
}
|
|
|
|
group.notify(queue: .main) {
|
|
self.attributedText = mutAttrString
|
|
self.setNeedsLayout()
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
// Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m
|
|
func createEmojiTextAttachment(image: UIImage, index: Int) -> NSTextAttachment {
|
|
let font = self.font!
|
|
|
|
let adjustedCapHeight = font.capHeight - 1
|
|
var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight)
|
|
|
|
let defaultScale: CGFloat = 1.4
|
|
imageSizeMatchingFontSize = CGSize(width: imageSizeMatchingFontSize.width * defaultScale, height: imageSizeMatchingFontSize.height * defaultScale)
|
|
let textColor = self.textColor!
|
|
|
|
UIGraphicsBeginImageContextWithOptions(imageSizeMatchingFontSize, false, 0.0)
|
|
textColor.set()
|
|
image.draw(in: CGRect(origin: .zero, size: imageSizeMatchingFontSize))
|
|
let attachmentImage = UIGraphicsGetImageFromCurrentImageContext()
|
|
UIGraphicsEndImageContext()
|
|
|
|
let attachment = NSTextAttachment()
|
|
attachment.image = attachmentImage
|
|
return attachment
|
|
}
|
|
|
|
// MARK: - HTML Parsing
|
|
func setTextFromHtml(_ html: String) {
|
|
let doc = try! SwiftSoup.parse(html)
|
|
let body = doc.body()!
|
|
|
|
let (attributedText, links) = attributedTextForHTMLNode(body)
|
|
let mutAttrString = NSMutableAttributedString(attributedString: attributedText)
|
|
|
|
// only trailing whitespace can be trimmed here
|
|
// when posting an attachment without any text, pleromafe includes U+200B ZERO WIDTH SPACE at the beginning
|
|
// this would get trimmed and cause range out of bounds crashes
|
|
mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
|
|
|
|
mutAttrString.addAttribute(.font, value: font!, range: NSRange(location: 0, length: mutAttrString.length))
|
|
|
|
self.links = []
|
|
let linkAttributes: [NSAttributedString.Key: Any] = [
|
|
.foregroundColor: UIColor.blue,
|
|
]
|
|
for (range, url) in links {
|
|
mutAttrString.addAttributes(linkAttributes, range: range)
|
|
self.links.append(Link(range: range, url: url))
|
|
}
|
|
|
|
self.attributedText = mutAttrString
|
|
}
|
|
|
|
private func attributedTextForHTMLNode(_ 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()
|
|
for child in node.getChildNodes() {
|
|
let (text, childLinks) = attributedTextForHTMLNode(child)
|
|
for (range, url) in childLinks {
|
|
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)
|
|
links[linkRange] = url
|
|
}
|
|
case "p":
|
|
attributed.append(NSAttributedString(string: "\n\n"))
|
|
default:
|
|
break
|
|
}
|
|
|
|
return (attributed, links)
|
|
default:
|
|
fatalError("Unexpected node type: \(type(of: node))")
|
|
}
|
|
}
|
|
|
|
func getViewController(forLink url: URL, inRange range: NSRange) -> UIViewController {
|
|
let text = (self.text! as NSString).substring(with: range)
|
|
|
|
if let mention = getMention(for: url, text: text) {
|
|
return ProfileTableViewController(accountID: mention.id)
|
|
} else if let tag = getHashtag(for: url, text: text) {
|
|
return TimelineTableViewController(for: .tag(hashtag: tag.name))
|
|
} else {
|
|
return SFSafariViewController(url: url)
|
|
}
|
|
}
|
|
|
|
func getViewController(forLinkAt point: CGPoint) -> UIViewController? {
|
|
guard let link = getLink(atPoint: point) else {
|
|
return nil
|
|
}
|
|
return getViewController(forLink: link.url, inRange: link.range)
|
|
}
|
|
|
|
// MARK: - Interaction
|
|
|
|
override func linkTapped(_ link: LinkLabel.Link) {
|
|
let text = (self.text! as NSString).substring(with: link.range)
|
|
|
|
if let mention = getMention(for: link.url, text: text) {
|
|
navigationDelegate?.selected(mention: mention)
|
|
} else if let tag = getHashtag(for: link.url, text: text) {
|
|
navigationDelegate?.selected(tag: tag)
|
|
} else {
|
|
navigationDelegate?.selected(url: link.url)
|
|
}
|
|
}
|
|
|
|
override func linkLongPressed(_ link: LinkLabel.Link) {
|
|
navigationDelegate?.showMoreOptions(forURL: link.url)
|
|
}
|
|
|
|
// MARK: - Navigation
|
|
func getMention(for url: URL, text: String) -> Mention? {
|
|
return nil
|
|
}
|
|
|
|
func getHashtag(for url: URL, text: String) -> Hashtag? {
|
|
if text.starts(with: "#") {
|
|
let tag = String(text.dropFirst())
|
|
return Hashtag(name: tag, url: url)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
}
|