mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			112 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Swift
		
	
		
		
			
		
	
	
			112 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Swift
		
	
| 
											3 years ago
										 | 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() | ||
|  |     } | ||
|  | } |