import Foundation 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() } }