|
|
// 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:ignore
|
|
|
.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
|
|
|
}
|
|
|
}
|