// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation import GRDB import DifferenceKit import SessionMessagingKit public struct EmojiWithSkinTones: Hashable, Equatable, ContentEquatable, ContentIdentifiable { let baseEmoji: Emoji? let skinTones: [Emoji.SkinTone]? let unsupportedValue: String? init(baseEmoji: Emoji, skinTones: [Emoji.SkinTone]? = nil) { self.baseEmoji = baseEmoji // Deduplicate skin tones, while preserving order. This allows for // multi-skin tone emoji, where if you have for example the permutation // [.dark, .dark], it is consolidated to just [.dark], to be initialized // with either variant and result in the correct emoji. self.skinTones = skinTones?.reduce(into: [Emoji.SkinTone]()) { result, skinTone in guard !result.contains(skinTone) else { return } result.append(skinTone) } self.unsupportedValue = nil } init(unsupportedValue: String) { self.unsupportedValue = unsupportedValue self.baseEmoji = nil self.skinTones = nil } var rawValue: String { if let baseEmoji = baseEmoji { if let skinTones = skinTones { return baseEmoji.emojiPerSkinTonePermutation?[skinTones] ?? baseEmoji.rawValue } else { return baseEmoji.rawValue } } if let unsupportedValue = unsupportedValue { return unsupportedValue } return "" // Should not happen } var normalized: EmojiWithSkinTones { if let baseEmoji = baseEmoji, baseEmoji.normalized != baseEmoji { return EmojiWithSkinTones(baseEmoji: baseEmoji.normalized) } return self } var isNormalized: Bool { self == normalized } } extension Emoji { static func getRecent(_ db: Database, withDefaultEmoji: Bool) throws -> [String] { let recentReactionEmoji: [String] = (db[.recentReactionEmoji]? .components(separatedBy: ",")) .defaulting(to: []) // No need to continue if we don't want the default emoji to pad out the list guard withDefaultEmoji else { return recentReactionEmoji } // Add in our default emoji if desired let defaultEmoji = ["😂", "🥰", "😢", "😡", "😮", "😈"] // stringlint:disable .filter { !recentReactionEmoji.contains($0) } return Array(recentReactionEmoji .appending(contentsOf: defaultEmoji) .prefix(6)) } static func addRecent(_ db: Database, emoji: String) { // Add/move the emoji to the start of the most recent list db[.recentReactionEmoji] = (db[.recentReactionEmoji]? .components(separatedBy: ",")) .defaulting(to: []) .filter { $0 != emoji } .inserting(emoji, at: 0) .prefix(6) .joined(separator: ",") } static func allSendableEmojiByCategoryWithPreferredSkinTones(_ db: Database) -> [Category: [EmojiWithSkinTones]] { return Category.allCases .reduce(into: [Category: [EmojiWithSkinTones]]()) { result, category in result[category] = category.normalizedEmoji .filter { $0.available } .map { $0.withPreferredSkinTones(db) } } } private func withPreferredSkinTones(_ db: Database) -> EmojiWithSkinTones { guard let rawSkinTones: String = db[.emojiPreferredSkinTones(emoji: rawValue)] else { return EmojiWithSkinTones(baseEmoji: self, skinTones: nil) } return EmojiWithSkinTones( baseEmoji: self, skinTones: rawSkinTones .split(separator: ",") .compactMap { SkinTone(rawValue: String($0)) } ) } func setPreferredSkinTones(_ db: Database, preferredSkinTonePermutation: [SkinTone]?) { db[.emojiPreferredSkinTones(emoji: rawValue)] = preferredSkinTonePermutation .map { preferredSkinTonePermutation in preferredSkinTonePermutation .map { $0.rawValue } .joined(separator: ",") } } init?(_ string: String) { guard let emojiWithSkinTonePermutation = EmojiWithSkinTones(rawValue: string) else { return nil } if let baseEmoji = emojiWithSkinTonePermutation.baseEmoji { self = baseEmoji } else { return nil } } } // MARK: - extension String { // This is slightly more accurate than String.isSingleEmoji, // but slower. // // * This will reject "lone modifiers". // * This will reject certain edge cases such as 🌈️. var isSingleEmojiUsingEmojiWithSkinTones: Bool { EmojiWithSkinTones(rawValue: self) != nil } }