diff --git a/Tusker/Caching/ImageCache.swift b/Tusker/Caching/ImageCache.swift index d5ec9d90..9b5cdd05 100644 --- a/Tusker/Caching/ImageCache.swift +++ b/Tusker/Caching/ImageCache.swift @@ -37,16 +37,20 @@ class ImageCache { completion?(entry.data, entry.image) return nil } else { - return Task.detached(priority: .userInitiated) { - let result = await self.fetch(url: url) - switch result { - case .data(let data): - completion?(data, nil) - case .dataAndImage(let data, let image): - completion?(data, image) - case .none: - completion?(nil, nil) - } + return getFromSource(url, completion: completion) + } + } + + func getFromSource(_ url: URL, completion: ((Data?, UIImage?) -> Void)?) -> Request? { + return Task.detached(priority: .userInitiated) { + let result = await self.fetch(url: url) + switch result { + case .data(let data): + completion?(data, nil) + case .dataAndImage(let data, let image): + completion?(data, image) + case .none: + completion?(nil, nil) } } } diff --git a/Tusker/Extensions/NSTextAttachment+Emoji.swift b/Tusker/Extensions/NSTextAttachment+Emoji.swift index ec056e7b..3c3059cd 100644 --- a/Tusker/Extensions/NSTextAttachment+Emoji.swift +++ b/Tusker/Extensions/NSTextAttachment+Emoji.swift @@ -10,6 +10,7 @@ import UIKit extension NSTextAttachment { // Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m + @available(iOS, deprecated: 15.0) convenience init(emojiImage image: UIImage, in font: UIFont, with textColor: UIColor = .label) { let adjustedCapHeight = font.capHeight - 1 var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight) diff --git a/Tusker/Views/BaseEmojiLabel.swift b/Tusker/Views/BaseEmojiLabel.swift index 7b53cada..b8f8e551 100644 --- a/Tusker/Views/BaseEmojiLabel.swift +++ b/Tusker/Views/BaseEmojiLabel.swift @@ -40,6 +40,16 @@ extension BaseEmojiLabel { return } + // Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m + let adjustedCapHeight = emojiFont.capHeight - 1 + func emojiImageSize(_ image: UIImage) -> CGSize { + var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight) + var scale: CGFloat = 1.4 + scale *= UIScreen.main.scale + imageSizeMatchingFontSize = CGSize(width: imageSizeMatchingFontSize.width * scale, height: imageSizeMatchingFontSize.height * scale) + return imageSizeMatchingFontSize + } + let emojiImages = MultiThreadDictionary() var foundEmojis = false @@ -57,21 +67,35 @@ extension BaseEmojiLabel { foundEmojis = true if let image = ImageCache.emojis.get(URL(emoji.url)!)?.image { - // if the image is cached, add it immediately - emojiImages[emoji.shortcode] = image + // if the image is cached, add it immediately. + // we generate the thumbnail on the main thread, because it's usually fast enough + // and the delay caused by doing it asynchronously looks works. + // todo: consider caching these somewhere? the cache needs to take into account both the url and the font size, which may vary across labels, so can't just use ImageCache + if let thumbnail = image.preparingThumbnail(of: emojiImageSize(image)), + let cgImage = thumbnail.cgImage { + // the thumbnail API takes a pixel size and returns an image with scale 1, but we want the actual screen scale, so convert + // see FB12187798 + emojiImages[emoji.shortcode] = UIImage(cgImage: cgImage, scale: UIScreen.main.scale, orientation: .up) + } } else { // otherwise, perform the network request group.enter() - // todo: ImageCache.emojis.get here will re-check the memory and disk caches, there should be another method to force-refetch - let request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in - guard let image = image, - let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(emoji.url)!, image: image) else { - group.leave() - return + let request = ImageCache.emojis.getFromSource(URL(emoji.url)!) { (_, image) in + guard let image else { + group.leave() + return + } + image.prepareThumbnail(of: emojiImageSize(image)) { thumbnail in + guard let thumbnail = thumbnail?.cgImage, + case let rescaled = UIImage(cgImage: thumbnail, scale: UIScreen.main.scale, orientation: .up), + let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(emoji.url)!, image: rescaled) else { + group.leave() + return + } + emojiImages[emoji.shortcode] = transformedImage + group.leave() } - emojiImages[emoji.shortcode] = transformedImage - group.leave() } if let request = request { emojiRequests.append(request) @@ -91,10 +115,8 @@ extension BaseEmojiLabel { // even though the closures is invoked on the same thread that withLock is called, so it's unclear why it needs to be @Sendable (FB11494878) // so, just ignore the warnings let emojiAttachments = emojiImages.withLock { - let emojiFont = self.emojiFont - let emojiTextColor = self.emojiTextColor return $0.mapValues { image in - NSTextAttachment(emojiImage: image, in: emojiFont, with: emojiTextColor) + NSTextAttachment(image: image) } } let placeholder = usePlaceholders ? NSTextAttachment(emojiPlaceholderIn: self.emojiFont) : nil