session-ios/Session/Emoji/Emoji+Available.swift

114 lines
4.7 KiB
Swift

import Foundation
import SignalCoreKit
import SessionUtilitiesKit
extension Emoji {
private static let availableCache: Atomic<[Emoji:Bool]> = Atomic([:])
private static let iosVersionKey = "iosVersion"
private static let cacheUrl = URL(fileURLWithPath: OWSFileSystem.appSharedDataDirectoryPath())
.appendingPathComponent("Library")
.appendingPathComponent("Caches")
.appendingPathComponent("emoji.plist")
static func warmAvailableCache() {
owsAssertDebug(!Thread.isMainThread)
guard CurrentAppContext().isMainAppAndActive else { return }
var availableCache = [Emoji: Bool]()
var uncachedEmoji = [Emoji]()
let iosVersion = UIDevice.current.systemVersion
// Use an NSMutableDictionary for built-in plist serialization and heterogeneous values.
var availableMap = NSMutableDictionary()
do {
availableMap = try NSMutableDictionary(contentsOf: Self.cacheUrl, error: ())
} catch {
Logger.info("Re-building emoji availability cache. Cache could not be loaded. \(error)")
uncachedEmoji = Emoji.allCases
}
let lastIosVersion = availableMap[iosVersionKey] as? String
if lastIosVersion == iosVersion {
Logger.debug("Loading emoji availability cache (expect \(Emoji.allCases.count) items, found \(availableMap.count - 1)).")
for emoji in Emoji.allCases {
if let available = availableMap[emoji.rawValue] as? Bool {
availableCache[emoji] = available
} else {
Logger.warn("Emoji unexpectedly missing from cache: \(emoji).")
uncachedEmoji.append(emoji)
}
}
} else if uncachedEmoji.isEmpty {
Logger.info("Re-building emoji availability cache. iOS version upgraded from \(lastIosVersion ?? "(none)") -> \(iosVersion)")
uncachedEmoji = Emoji.allCases
}
if !uncachedEmoji.isEmpty {
Logger.info("Checking emoji availability for \(uncachedEmoji.count) uncached emoji")
uncachedEmoji.forEach {
let available = isEmojiAvailable($0)
availableMap[$0.rawValue] = available
availableCache[$0] = available
}
availableMap[iosVersionKey] = iosVersion
do {
// Use FileManager.createDirectory directly because OWSFileSystem.ensureDirectoryExists
// can modify the protection, and this is a system-managed directory.
try FileManager.default.createDirectory(at: Self.cacheUrl.deletingLastPathComponent(),
withIntermediateDirectories: true)
try availableMap.write(to: Self.cacheUrl)
} catch {
Logger.warn("Failed to save emoji availability cache; it will be recomputed next time! \(error)")
}
}
Logger.info("Warmed emoji availability cache with \(availableCache.lazy.filter { $0.value }.count) available emoji for iOS \(iosVersion)")
Self.availableCache.mutate{ $0 = availableCache }
}
private static func isEmojiAvailable(_ emoji: Emoji) -> Bool {
return emoji.rawValue.isUnicodeStringAvailable
}
/// Indicates whether the given emoji is available on this iOS
/// version. We cache the availability in memory.
var available: Bool {
guard let available = Self.availableCache.wrappedValue[self] else {
let available = Self.isEmojiAvailable(self)
Self.availableCache.mutate{ $0[self] = available }
return available
}
return available
}
}
private extension String {
/// A known undefined unicode character for comparison
private static let unknownUnicodeStringPng = "\u{1fff}".unicodeStringPngRepresentation
// Based on https://stackoverflow.com/a/41393387
// Check if an emoji is available on the current iOS version
// by verifying its image is different than the "unknown"
// reference image
var isUnicodeStringAvailable: Bool {
guard self.isSingleEmoji else { return false }
return String.unknownUnicodeStringPng != unicodeStringPngRepresentation
}
var unicodeStringPngRepresentation: Data? {
let attributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 8)]
let size = (self as NSString).size(withAttributes: attributes)
UIGraphicsBeginImageContext(size)
defer { UIGraphicsEndImageContext() }
(self as NSString).draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
guard let unicodeImage = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
return unicodeImage.pngData()
}
}