diff --git a/Pachyderm/Model/Emoji.swift b/Pachyderm/Model/Emoji.swift index 2ea10b60..bec0ea9d 100644 --- a/Pachyderm/Model/Emoji.swift +++ b/Pachyderm/Model/Emoji.swift @@ -9,17 +9,16 @@ import Foundation public class Emoji: Decodable { - let shortcode: String - let url: URL - let staticURL: URL - // TODO: missing in pleroma -// let visibleInPicker: Bool + public let shortcode: String + public let url: URL + public let staticURL: URL + public let visibleInPicker: Bool private enum CodingKeys: String, CodingKey { case shortcode case url case staticURL = "static_url" -// case visibleInPicker = "visible_in_picker" + case visibleInPicker = "visible_in_picker" } } diff --git a/Tusker/Caching/ImageCache.swift b/Tusker/Caching/ImageCache.swift index b86c8a52..c6b8a586 100644 --- a/Tusker/Caching/ImageCache.swift +++ b/Tusker/Caching/ImageCache.swift @@ -14,6 +14,7 @@ class ImageCache { static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24)) static let headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24)) static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2)) + static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60)) let cache: Cache diff --git a/Tusker/Views/ContentLabel.swift b/Tusker/Views/ContentLabel.swift index 9ec06335..75aac6e6 100644 --- a/Tusker/Views/ContentLabel.swift +++ b/Tusker/Views/ContentLabel.swift @@ -8,14 +8,74 @@ import UIKit import SafariServices -import TTTAttributedLabel 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) diff --git a/Tusker/Views/StatusContentLabel.swift b/Tusker/Views/StatusContentLabel.swift index a32dfeb4..5c113e74 100644 --- a/Tusker/Views/StatusContentLabel.swift +++ b/Tusker/Views/StatusContentLabel.swift @@ -16,6 +16,7 @@ class StatusContentLabel: ContentLabel { guard let statusID = statusID else { return } guard let status = MastodonCache.status(for: statusID) else { fatalError("Can't set StatusContentLabel text without cached status \(statusID)") } setTextFromHtml(status.content) + setEmojis(status.emojis) } }