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.
session-ios/Session/Emoji/Emoji+Available.swift

120 lines
5.0 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
Merge remote-tracking branch 'origin/feature/swift-package-manager' into feature/groups-rebuild # Conflicts: # Podfile # Podfile.lock # Session.xcodeproj/project.pbxproj # Session/Calls/Call Management/SessionCall.swift # Session/Calls/Call Management/SessionCallManager.swift # Session/Calls/CallVC.swift # Session/Conversations/ConversationVC+Interaction.swift # Session/Conversations/ConversationVC.swift # Session/Conversations/ConversationViewModel.swift # Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift # Session/Conversations/Settings/ThreadSettingsViewModel.swift # Session/Emoji/Emoji+Available.swift # Session/Home/GlobalSearch/GlobalSearchViewController.swift # Session/Home/HomeVC.swift # Session/Home/HomeViewModel.swift # Session/Home/New Conversation/NewDMVC.swift # Session/Media Viewing & Editing/DocumentTitleViewController.swift # Session/Media Viewing & Editing/GIFs/GifPickerCell.swift # Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift # Session/Media Viewing & Editing/ImagePickerController.swift # Session/Media Viewing & Editing/MediaTileViewController.swift # Session/Media Viewing & Editing/PhotoCapture.swift # Session/Media Viewing & Editing/PhotoCaptureViewController.swift # Session/Media Viewing & Editing/PhotoLibrary.swift # Session/Media Viewing & Editing/SendMediaNavigationController.swift # Session/Meta/AppDelegate.swift # Session/Meta/AppEnvironment.swift # Session/Meta/MainAppContext.swift # Session/Meta/SessionApp.swift # Session/Notifications/NotificationPresenter.swift # Session/Notifications/PushRegistrationManager.swift # Session/Notifications/SyncPushTokensJob.swift # Session/Notifications/UserNotificationsAdaptee.swift # Session/Onboarding/LandingVC.swift # Session/Onboarding/LinkDeviceVC.swift # Session/Onboarding/Onboarding.swift # Session/Onboarding/RegisterVC.swift # Session/Onboarding/RestoreVC.swift # Session/Settings/HelpViewModel.swift # Session/Settings/NukeDataModal.swift # Session/Shared/FullConversationCell.swift # Session/Shared/OWSBezierPathView.m # Session/Utilities/BackgroundPoller.swift # Session/Utilities/MockDataGenerator.swift # SessionMessagingKit/Configuration.swift # SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift # SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift # SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift # SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift # SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift # SessionMessagingKit/Database/Models/Attachment.swift # SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift # SessionMessagingKit/Database/Models/Interaction.swift # SessionMessagingKit/Database/Models/Profile.swift # SessionMessagingKit/Database/Models/SessionThread.swift # SessionMessagingKit/File Server/FileServerAPI.swift # SessionMessagingKit/Jobs/AttachmentDownloadJob.swift # SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift # SessionMessagingKit/Jobs/DisappearingMessagesJob.swift # SessionMessagingKit/Jobs/FailedMessageSendsJob.swift # SessionMessagingKit/Jobs/MessageSendJob.swift # SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift # SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift # SessionMessagingKit/Messages/Message.swift # SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroupAPI.swift # SessionMessagingKit/Open Groups/Models/SOGSMessage.swift # SessionMessagingKit/Open Groups/OpenGroupAPI.swift # SessionMessagingKit/Open Groups/OpenGroupManager.swift # SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+LegacyClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver.swift # SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift # SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift # SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift # SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupAPI+Poller.swift # SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift # SessionMessagingKit/Utilities/ProfileManager.swift # SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift # SessionMessagingKitTests/LibSession/LibSessionSpec.swift # SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift # SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift # SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift # SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift # SessionMessagingKitTests/Utilities/CryptoSMKSpec.swift # SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift # SessionNotificationServiceExtension/NSENotificationPresenter.swift # SessionNotificationServiceExtension/NotificationServiceExtension.swift # SessionShareExtension/ShareAppExtensionContext.swift # SessionShareExtension/ShareNavController.swift # SessionShareExtension/ThreadPickerVC.swift # SessionSnodeKit/Crypto/Crypto+SessionSnodeKit.swift # SessionSnodeKit/Models/DeleteAllBeforeResponse.swift # SessionSnodeKit/Models/DeleteAllMessagesResponse.swift # SessionSnodeKit/Models/DeleteMessagesResponse.swift # SessionSnodeKit/Models/RevokeSubkeyRequest.swift # SessionSnodeKit/Models/RevokeSubkeyResponse.swift # SessionSnodeKit/Models/SendMessageResponse.swift # SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift # SessionSnodeKit/Models/UpdateExpiryAllResponse.swift # SessionSnodeKit/Models/UpdateExpiryResponse.swift # SessionSnodeKit/Networking/SnodeAPI.swift # SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift # SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift # SessionTests/Database/DatabaseSpec.swift # SessionTests/Settings/NotificationContentViewModelSpec.swift # SessionUIKit/Components/ToastController.swift # SessionUIKit/Style Guide/Values.swift # SessionUtilitiesKit/Crypto/Crypto+SessionUtilitiesKit.swift # SessionUtilitiesKit/Crypto/Crypto.swift # SessionUtilitiesKit/Database/Models/Identity.swift # SessionUtilitiesKit/Database/Models/Job.swift # SessionUtilitiesKit/Database/Storage.swift # SessionUtilitiesKit/Database/Types/Migration.swift # SessionUtilitiesKit/General/AppContext.swift # SessionUtilitiesKit/General/Data+Utilities.swift # SessionUtilitiesKit/General/Logging.swift # SessionUtilitiesKit/General/SNUserDefaults.swift # SessionUtilitiesKit/General/String+Trimming.swift # SessionUtilitiesKit/General/String+Utilities.swift # SessionUtilitiesKit/General/TimestampUtils.swift # SessionUtilitiesKit/General/UIEdgeInsets.swift # SessionUtilitiesKit/JobRunner/JobRunner.swift # SessionUtilitiesKit/LibSession/LibSessionError.swift # SessionUtilitiesKit/Media/DataSource.swift # SessionUtilitiesKit/Meta/SessionUtilitiesKit.h # SessionUtilitiesKit/Networking/NetworkType.swift # SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift # SessionUtilitiesKit/Utilities/BackgroundTaskManager.swift # SessionUtilitiesKit/Utilities/BencodeResponse.swift # SessionUtilitiesKit/Utilities/CExceptionHelper.mm # SessionUtilitiesKit/Utilities/FileManagerType.swift # SessionUtilitiesKit/Utilities/KeychainStorageType.swift # SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift # SessionUtilitiesKitTests/Database/Utilities/PersistableRecordUtilitiesSpec.swift # SessionUtilitiesKitTests/General/SessionIdSpec.swift # SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift # SignalUtilitiesKit/Configuration.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalInputAccessoryView.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift # SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCropViewController.swift # SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift # SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift # SignalUtilitiesKit/Meta/SignalUtilitiesKit.h # SignalUtilitiesKit/Shared View Controllers/OWSViewController.swift # SignalUtilitiesKit/Shared Views/CircleView.swift # SignalUtilitiesKit/Shared Views/TappableView.swift # SignalUtilitiesKit/Utilities/AppSetup.swift # SignalUtilitiesKit/Utilities/Bench.swift # SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift # _SharedTestUtilities/CommonMockedExtensions.swift # _SharedTestUtilities/MockCrypto.swift # _SharedTestUtilities/Mocked.swift # _SharedTestUtilities/SynchronousStorage.swift
10 months ago
import UIKit
import SessionUtilitiesKit
extension Emoji {
private static let availableCache: Atomic<[Emoji:Bool]> = Atomic([:])
private static let iosVersionKey = "iosVersion"
private static let cacheUrl = URL(fileURLWithPath: FileManager.default.appSharedDataDirectoryPath)
.appendingPathComponent("Library")
.appendingPathComponent("Caches")
.appendingPathComponent("emoji.plist")
static func warmAvailableCache(using dependencies: Dependencies) {
Log.assertOnMainThread()
guard
dependencies.hasInitialised(singleton: .appContext) &&
dependencies[singleton: .appContext].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 {
Log.info("[Emoji] Re-building emoji availability cache. Cache could not be loaded. \(error)")
uncachedEmoji = Emoji.allCases
}
let lastIosVersion = availableMap[iosVersionKey] as? String
if lastIosVersion == iosVersion {
Log.debug("[Emoji] 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 {
Log.warn("[Emoji] Emoji unexpectedly missing from cache: \(emoji).")
uncachedEmoji.append(emoji)
}
}
} else if uncachedEmoji.isEmpty {
Log.info("[Emoji] Re-building emoji availability cache. iOS version upgraded from \(lastIosVersion ?? "(none)") -> \(iosVersion)")
uncachedEmoji = Emoji.allCases
}
if !uncachedEmoji.isEmpty {
Log.info("[Emoji] 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 FileSystem.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 {
Log.warn("[Emoji] Failed to save emoji availability cache; it will be recomputed next time! \(error)")
}
}
Log.info("[Emoji] 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()
}
}