diff --git a/Scripts/EmojiGenerator.swift b/Scripts/EmojiGenerator.swift new file mode 100755 index 000000000..44f906cc1 --- /dev/null +++ b/Scripts/EmojiGenerator.swift @@ -0,0 +1,646 @@ +#!/usr/bin/env xcrun --sdk macosx swift + +import Foundation + +// OWSAssertionError but for this script + +enum EmojiError: Error { + case assertion(String) + init(_ string: String) { + self = .assertion(string) + } +} + +// MARK: - Remote Model +// These definitions are kept fairly lightweight since we don't control their format +// All processing of remote data is done by converting RemoteModel items to EmojiModel items + +enum RemoteModel { + struct EmojiItem: Codable { + let name: String + let shortName: String + let unified: String + let sortOrder: UInt + let category: EmojiCategory + let skinVariations: [String: SkinVariation]? + let shortNames: [String]? + } + + struct SkinVariation: Codable { + let unified: String + } + + enum EmojiCategory: String, Codable, Equatable { + case smileys = "Smileys & Emotion" + case people = "People & Body" + + // This category is not provided in the data set, but is actually + // a merger of the categories of `smileys` and `people` + case smileysAndPeople = "Smileys & People" + + case animals = "Animals & Nature" + case food = "Food & Drink" + case activities = "Activities" + case travel = "Travel & Places" + case objects = "Objects" + case symbols = "Symbols" + case flags = "Flags" + case components = "Component" + } + + static func fetchEmojiData() throws -> Data { + // let remoteSourceUrl = URL(string: "https://unicodey.com/emoji-data/emoji.json")! + // This URL has been unavailable the past couple of weeks. If you're seeing failures here, try this other one: + let remoteSourceUrl = URL(string: "https://raw.githubusercontent.com/iamcal/emoji-data/master/emoji.json")! + return try Data(contentsOf: remoteSourceUrl) + } +} + +// MARK: - Local Model + +struct EmojiModel { + let definitions: [EmojiDefinition] + + struct EmojiDefinition { + let category: RemoteModel.EmojiCategory + let rawName: String + let enumName: String + var shortNames: Set + let variants: [Emoji] + var baseEmoji: Character { variants[0].base } + + struct Emoji: Comparable { + let emojiChar: Character + + let base: Character + let skintoneSequence: SkinToneSequence + + static func <(lhs: Self, rhs: Self) -> Bool { + for (leftElement, rightElement) in zip(lhs.skintoneSequence, rhs.skintoneSequence) { + if leftElement.sortId != rightElement.sortId { + return leftElement.sortId < rightElement.sortId + } + } + if lhs.skintoneSequence.count != rhs.skintoneSequence.count { + return lhs.skintoneSequence.count < rhs.skintoneSequence.count + } else { + return false + } + } + } + + init(parsingRemoteItem remoteItem: RemoteModel.EmojiItem) throws { + category = remoteItem.category + rawName = remoteItem.name + enumName = Self.parseEnumNameFromRemoteItem(remoteItem) + shortNames = Set((remoteItem.shortNames ?? [])) + shortNames.insert(rawName.lowercased()) + shortNames.insert(enumName.lowercased()) + + let baseEmojiChar = try Self.codePointsToCharacter(Self.parseCodePointString(remoteItem.unified)) + let baseEmoji = Emoji(emojiChar: baseEmojiChar, base: baseEmojiChar, skintoneSequence: .none) + + let toneVariants: [Emoji] + if let skinVariations = remoteItem.skinVariations { + toneVariants = try skinVariations.map { key, value in + let modifier = SkinTone.sequence(from: Self.parseCodePointString(key)) + let parsedEmoji = try Self.codePointsToCharacter(Self.parseCodePointString(value.unified)) + return Emoji(emojiChar: parsedEmoji, base: baseEmojiChar, skintoneSequence: modifier) + }.sorted() + } else { + toneVariants = [] + } + + variants = [baseEmoji] + toneVariants + try postInitValidation() + } + + func postInitValidation() throws { + guard variants.count > 0 else { + throw EmojiError("Expecting at least one variant") + } + + guard variants.allSatisfy({ $0.base == baseEmoji }) else { + // All emoji variants must have a common base emoji + throw EmojiError("Inconsistent base emoji: \(baseEmoji)") + } + + let hasMultipleComponents = variants.first(where: { $0.skintoneSequence.count > 1 }) != nil + if hasMultipleComponents, skinToneComponents == nil { + // If you hit this, this means a new emoji was added where a skintone modifier sequence specifies multiple + // skin tones for multiple emoji components: e.g. đŸ‘Ģ -> 🧍‍♀ī¸+🧍‍♂ī¸ + // These are defined in `skinToneComponents`. You'll need to add a new case. + throw EmojiError("\(baseEmoji):\(enumName) definition has variants with multiple skintone modifiers but no component emojis defined") + } + } + + static func parseEnumNameFromRemoteItem(_ item: RemoteModel.EmojiItem) -> String { + // some names don't play nice with swift, so we special case them + switch item.shortName { + case "+1": return "plusOne" + case "-1": return "negativeOne" + case "8ball": return "eightBall" + case "repeat": return "`repeat`" + case "100": return "oneHundred" + case "1234": return "oneTwoThreeFour" + case "couplekiss": return "personKissPerson" + case "couple_with_heart": return "personHeartPerson" + default: + let uppperCamelCase = item.shortName + .replacingOccurrences(of: "-", with: " ") + .replacingOccurrences(of: "_", with: " ") + .titlecase + .replacingOccurrences(of: " ", with: "") + + return uppperCamelCase.first!.lowercased() + uppperCamelCase.dropFirst() + } + } + + var skinToneComponents: String? { + // There's no great way to do this except manually. Some emoji have multiple skin tones. + // In the picker, we need to use one emoji to represent each person. For now, we manually + // specify this. Hopefully, in the future, the data set will contain this information. + switch enumName { + case "peopleHoldingHands": return "[.standingPerson, .standingPerson]" + case "twoWomenHoldingHands": return "[.womanStanding, .womanStanding]" + case "manAndWomanHoldingHands": return "[.womanStanding, .manStanding]" + case "twoMenHoldingHands": return "[.manStanding, .manStanding]" + case "personKissPerson": return "[.adult, .adult]" + case "womanKissMan": return "[.woman, .man]" + case "manKissMan": return "[.man, .man]" + case "womanKissWoman": return "[.woman, .woman]" + case "personHeartPerson": return "[.adult, .adult]" + case "womanHeartMan": return "[.woman, .man]" + case "manHeartMan": return "[.man, .man]" + case "womanHeartWoman": return "[.woman, .woman]" + case "handshake": return "[.rightwardsHand, .leftwardsHand]" + default: + return nil + } + } + + var isNormalized: Bool { enumName == normalizedEnumName } + var normalizedEnumName: String { + switch enumName { + // flagUm (US Minor Outlying Islands) looks identical to the + // US flag. We don't present it as a sendable reaction option + // This matches the iOS keyboard behavior. + case "flagUm": return "us" + default: return enumName + } + } + + static func parseCodePointString(_ pointString: String) -> [UnicodeScalar] { + return pointString + .components(separatedBy: "-") + .map { Int($0, radix: 16)! } + .map { UnicodeScalar($0)! } + } + + static func codePointsToCharacter(_ codepoints: [UnicodeScalar]) throws -> Character { + let result = codepoints.map { String($0) }.joined() + if result.count != 1 { + throw EmojiError("Invalid number of chars for codepoint sequence: \(codepoints)") + } + return result.first! + } + } + + init(rawJSONData jsonData: Data) throws { + let jsonDecoder = JSONDecoder() + jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase + + definitions = try jsonDecoder + .decode([RemoteModel.EmojiItem].self, from: jsonData) + .sorted { $0.sortOrder < $1.sortOrder } + .map { try EmojiDefinition(parsingRemoteItem: $0) } + + } + + typealias SkinToneSequence = [EmojiModel.SkinTone] + enum SkinTone: UnicodeScalar, CaseIterable, Equatable { + case light = "đŸģ" + case mediumLight = "đŸŧ" + case medium = "đŸŊ" + case mediumDark = "🏾" + case dark = "đŸŋ" + + var sortId: Int { return SkinTone.allCases.firstIndex(of: self)! } + + static func sequence(from codepoints: [UnicodeScalar]) -> SkinToneSequence { + codepoints + .map { SkinTone(rawValue: $0)! } + .reduce(into: [SkinTone]()) { result, skinTone in + guard !result.contains(skinTone) else { return } + result.append(skinTone) + } + } + } +} + +extension EmojiModel.SkinToneSequence { + static var none: EmojiModel.SkinToneSequence = [] +} + +// MARK: - File Writers + +extension EmojiGenerator { + static func writePrimaryFile(from emojiModel: EmojiModel) { + // Main enum: Create a string enum defining our enumNames equal to the baseEmoji string + // e.g. case grinning = "😀" + writeBlock(fileName: "Emoji.swift") { fileHandle in + fileHandle.writeLine("// swiftlint:disable all") + fileHandle.writeLine("") + fileHandle.writeLine("/// A sorted representation of all available emoji") + fileHandle.writeLine("enum Emoji: String, CaseIterable, Equatable {") + fileHandle.indent { + emojiModel.definitions.forEach { + fileHandle.writeLine("case \($0.enumName) = \"\($0.baseEmoji)\"") + } + } + fileHandle.writeLine("}") + fileHandle.writeLine("// swiftlint:disable all") + } + } + + static func writeStringConversionsFile(from emojiModel: EmojiModel) { + // Inline helpers: + var firstItem = true + func conditionalCheckForEmojiItem(_ item: EmojiModel.EmojiDefinition.Emoji) -> String { + let isFirst = (firstItem == true) + firstItem = false + + let prefix = isFirst ? "" : "} else " + let suffix = "if rawValue == \"\(item.emojiChar)\" {" + return prefix + suffix + } + func conversionForEmojiItem(_ item: EmojiModel.EmojiDefinition.Emoji, definition: EmojiModel.EmojiDefinition) -> String { + let skinToneString: String + if item.skintoneSequence.isEmpty { + skinToneString = "nil" + } else { + skinToneString = "[\(item.skintoneSequence.map { ".\($0)" }.joined(separator: ", "))]" + } + return "self.init(baseEmoji: .\(definition.enumName), skinTones: \(skinToneString))" + } + + // Conversion from String: Creates an initializer mapping a single character emoji string to an EmojiWithSkinTones + // e.g. + // if rawValue == "😀" { self.init(baseEmoji: .grinning, skinTones: nil) } + // else if rawValue == "đŸĻģđŸģ" { self.init(baseEmoji: .earWithHearingAid, skinTones: [.light]) + writeBlock(fileName: "EmojiWithSkinTones+String.swift") { fileHandle in + fileHandle.writeLine("extension EmojiWithSkinTones {") + fileHandle.indent { + fileHandle.writeLine("init?(rawValue: String) {") + fileHandle.indent { + fileHandle.writeLine("guard rawValue.isSingleEmoji else { return nil }") + + emojiModel.definitions.forEach { definition in + definition.variants.forEach { emoji in + fileHandle.writeLine(conditionalCheckForEmojiItem(emoji)) + fileHandle.indent { + fileHandle.writeLine(conversionForEmojiItem(emoji, definition: definition)) + } + } + } + + fileHandle.writeLine("} else {") + fileHandle.indent { + fileHandle.writeLine("self.init(unsupportedValue: rawValue)") + } + fileHandle.writeLine("}") + } + fileHandle.writeLine("}") + } + fileHandle.writeLine("}") + } + } + + static func writeSkinToneLookupFile(from emojiModel: EmojiModel) { + writeBlock(fileName: "Emoji+SkinTones.swift") { fileHandle in + fileHandle.writeLine("extension Emoji {") + fileHandle.indent { + // SkinTone enum + fileHandle.writeLine("enum SkinTone: String, CaseIterable, Equatable {") + fileHandle.indent { + for skinTone in EmojiModel.SkinTone.allCases { + fileHandle.writeLine("case \(skinTone) = \"\(skinTone.rawValue)\"") + } + } + fileHandle.writeLine("}") + fileHandle.writeLine("") + + // skin tone helpers + fileHandle.writeLine("var hasSkinTones: Bool { return emojiPerSkinTonePermutation != nil }") + fileHandle.writeLine("var allowsMultipleSkinTones: Bool { return hasSkinTones && skinToneComponentEmoji != nil }") + fileHandle.writeLine("") + + // Start skinToneComponentEmoji + fileHandle.writeLine("var skinToneComponentEmoji: [Emoji]? {") + fileHandle.indent { + fileHandle.writeLine("switch self {") + emojiModel.definitions.forEach { emojiDef in + if let components = emojiDef.skinToneComponents { + fileHandle.writeLine("case .\(emojiDef.enumName): return \(components)") + } + } + + fileHandle.writeLine("default: return nil") + fileHandle.writeLine("}") + } + fileHandle.writeLine("}") + fileHandle.writeLine("") + + // Start emojiPerSkinTonePermutation + fileHandle.writeLine("var emojiPerSkinTonePermutation: [[SkinTone]: String]? {") + fileHandle.indent { + fileHandle.writeLine("switch self {") + emojiModel.definitions.forEach { emojiDef in + let skintoneVariants = emojiDef.variants.filter({ $0.skintoneSequence != .none}) + if skintoneVariants.isEmpty { + // None of our variants have a skintone, nothing to do + return + } + + fileHandle.writeLine("case .\(emojiDef.enumName):") + fileHandle.indent { + fileHandle.writeLine("return [") + fileHandle.indent { + skintoneVariants.forEach { + let skintoneSequenceKey = $0.skintoneSequence.map({ ".\($0)" }).joined(separator: ", ") + fileHandle.writeLine("[\(skintoneSequenceKey)]: \"\($0.emojiChar)\",") + } + } + fileHandle.writeLine("]") + } + } + fileHandle.writeLine("default: return nil") + fileHandle.writeLine("}") + } + fileHandle.writeLine("}") + } + fileHandle.writeLine("}") + } + } + + static func writeCategoryLookupFile(from emojiModel: EmojiModel) { + let outputCategories: [RemoteModel.EmojiCategory] = [ + .smileysAndPeople, + .animals, + .food, + .activities, + .travel, + .objects, + .symbols, + .flags + ] + + writeBlock(fileName: "Emoji+Category.swift") { fileHandle in + fileHandle.writeLine("extension Emoji {") + fileHandle.indent { + + // Category enum + fileHandle.writeLine("enum Category: String, CaseIterable, Equatable {") + fileHandle.indent { + // Declare cases + for category in outputCategories { + fileHandle.writeLine("case \(category) = \"\(category.rawValue)\"") + } + fileHandle.writeLine("") + + // Localized name for category + fileHandle.writeLine("var localizedName: String {") + fileHandle.indent { + fileHandle.writeLine("switch self {") + for category in outputCategories { + fileHandle.writeLine("case .\(category):") + fileHandle.indent { + let stringKey = "EMOJI_CATEGORY_\("\(category)".uppercased())_NAME" + let stringComment = "The name for the emoji category '\(category.rawValue)'" + + fileHandle.writeLine("return NSLocalizedString(\"\(stringKey)\", comment: \"\(stringComment)\")") + } + } + fileHandle.writeLine("}") + } + fileHandle.writeLine("}") + fileHandle.writeLine("") + + // Emoji lookup per category + fileHandle.writeLine("var normalizedEmoji: [Emoji] {") + fileHandle.indent { + fileHandle.writeLine("switch self {") + + let normalizedEmojiPerCategory: [RemoteModel.EmojiCategory: [EmojiModel.EmojiDefinition]] + normalizedEmojiPerCategory = emojiModel.definitions.reduce(into: [:]) { result, emojiDef in + if emojiDef.isNormalized { + var categoryList = result[emojiDef.category] ?? [] + categoryList.append(emojiDef) + result[emojiDef.category] = categoryList + } + } + + for category in outputCategories { + let emoji: [EmojiModel.EmojiDefinition] = { + switch category { + case .smileysAndPeople: + // Merge smileys & people. It's important we initially bucket these separately, + // because we want the emojis to be sorted smileys followed by people + return normalizedEmojiPerCategory[.smileys]! + normalizedEmojiPerCategory[.people]! + default: + return normalizedEmojiPerCategory[category]! + } + }() + + fileHandle.writeLine("case .\(category):") + fileHandle.indent { + fileHandle.writeLine("return [") + fileHandle.indent { + emoji.compactMap { $0.enumName }.forEach { name in + fileHandle.writeLine(".\(name),") + } + } + fileHandle.writeLine("]") + } + } + fileHandle.writeLine("}") + } + fileHandle.writeLine("}") + } + fileHandle.writeLine("}") + fileHandle.writeLine("") + + // Category lookup per emoji + fileHandle.writeLine("var category: Category {") + fileHandle.indent { + fileHandle.writeLine("switch self {") + for emojiDef in emojiModel.definitions { + let category = [.smileys, .people].contains(emojiDef.category) ? .smileysAndPeople : emojiDef.category + if category != .components { + fileHandle.writeLine("case .\(emojiDef.enumName): return .\(category)") + } + } + // Write a default case, because this enum is too long for the compiler to validate it's exhaustive + fileHandle.writeLine("default: fatalError(\"Unexpected case \\(self)\")") + fileHandle.writeLine("}") + } + fileHandle.writeLine("}") + fileHandle.writeLine("") + + // Normalized variant mapping + fileHandle.writeLine("var isNormalized: Bool { normalized == self }") + fileHandle.writeLine("var normalized: Emoji {") + fileHandle.indent { + fileHandle.writeLine("switch self {") + emojiModel.definitions.filter { !$0.isNormalized }.forEach { + fileHandle.writeLine("case .\($0.enumName): return .\($0.normalizedEnumName)") + } + fileHandle.writeLine("default: return self") + fileHandle.writeLine("}") + } + fileHandle.writeLine("}") + } + fileHandle.writeLine("}") + } + } + + static func writeNameLookupFile(from emojiModel: EmojiModel) { + // Name lookup: Create a computed property mapping an Emoji enum element to the raw Emoji name string + // e.g. case .grinning: return "GRINNING FACE" + writeBlock(fileName: "Emoji+Name.swift") { fileHandle in + fileHandle.writeLine("extension Emoji {") + fileHandle.indent { + fileHandle.writeLine("var name: String {") + fileHandle.indent { + fileHandle.writeLine("switch self {") + emojiModel.definitions.forEach { + fileHandle.writeLine("case .\($0.enumName): return \"\($0.shortNames.joined(separator:", "))\"") + } + fileHandle.writeLine("}") + } + fileHandle.writeLine("}") + } + fileHandle.writeLine("}") + } + } +} + +// MARK: - File I/O Helpers + +class WriteHandle { + static let emojiDirectory = URL( + fileURLWithPath: "../Session/Emoji", + isDirectory: true, + relativeTo: EmojiGenerator.pathToFolderContainingThisScript!) + + let handle: FileHandle + + var indentDepth: Int = 0 + var hasBeenClosed = false + + func indent(_ block: () -> Void) { + indentDepth += 1 + block() + indentDepth -= 1 + } + + func writeLine(_ body: String) { + let spaces = indentDepth * 4 + let prefix = String(repeating: " ", count: spaces) + let suffix = "\n" + + let line = prefix + body + suffix + handle.write(line.data(using: .utf8)!) + } + + init(fileName: String) { + // Create directory if necessary + if !FileManager.default.fileExists(atPath: Self.emojiDirectory.path) { + try! FileManager.default.createDirectory(at: Self.emojiDirectory, withIntermediateDirectories: true, attributes: nil) + } + + // Delete old file and create anew + let url = URL(fileURLWithPath: fileName, relativeTo: Self.emojiDirectory) + if FileManager.default.fileExists(atPath: url.path) { + try! FileManager.default.removeItem(at: url) + } + FileManager.default.createFile(atPath: url.path, contents: nil, attributes: nil) + handle = try! FileHandle(forWritingTo: url) + } + + deinit { + precondition(hasBeenClosed, "File handle still open at de-init") + } + + func close() { + handle.closeFile() + hasBeenClosed = true + } +} + +extension EmojiGenerator { + static func writeBlock(fileName: String, block: (WriteHandle) -> Void) { + let fileHandle = WriteHandle(fileName: fileName) + defer { fileHandle.close() } + + fileHandle.writeLine("") + fileHandle.writeLine("// This file is generated by EmojiGenerator.swift, do not manually edit it.") + fileHandle.writeLine("") + + block(fileHandle) + } + + // from http://stackoverflow.com/a/31480534/255489 + static var pathToFolderContainingThisScript: URL? = { + let cwd = FileManager.default.currentDirectoryPath + + let script = CommandLine.arguments[0] + + if script.hasPrefix("/") { // absolute + let path = (script as NSString).deletingLastPathComponent + return URL(fileURLWithPath: path) + } else { // relative + let urlCwd = URL(fileURLWithPath: cwd) + + if let urlPath = URL(string: script, relativeTo: urlCwd) { + let path = (urlPath.path as NSString).deletingLastPathComponent + return URL(fileURLWithPath: path) + } + } + + return nil + }() +} + +// MARK: - Misc + +extension String { + var titlecase: String { + components(separatedBy: " ") + .map { $0.first!.uppercased() + $0.dropFirst().lowercased() } + .joined(separator: " ") + } +} + +// MARK: - Lifecycle + +class EmojiGenerator { + static func run() throws { + let remoteData = try RemoteModel.fetchEmojiData() + let model = try EmojiModel(rawJSONData: remoteData) + + writePrimaryFile(from: model) + writeStringConversionsFile(from: model) + writeSkinToneLookupFile(from: model) + writeCategoryLookupFile(from: model) + writeNameLookupFile(from: model) + } +} + +do { + try EmojiGenerator.run() +} catch { + print("Failed to generate emoji data: \(error)") + let errorCode = (error as? CustomNSError)?.errorCode ?? -1 + exit(Int32(errorCode)) +} diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 83eaaa839..2e86a414b 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -121,21 +121,39 @@ 7B1581E4271FC59D00848B49 /* CallModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1581E3271FC59C00848B49 /* CallModal.swift */; }; 7B1581E6271FD2A100848B49 /* VideoPreviewVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1581E5271FD2A100848B49 /* VideoPreviewVC.swift */; }; 7B1581E827210ECC00848B49 /* RenderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1581E727210ECC00848B49 /* RenderView.swift */; }; + 7B1B52D828580C6D006069F2 /* EmojiPickerSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52D728580C6D006069F2 /* EmojiPickerSheet.swift */; }; + 7B1B52DF28580D51006069F2 /* EmojiPickerCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52DB28580D50006069F2 /* EmojiPickerCollectionView.swift */; }; + 7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52DC28580D50006069F2 /* EmojiSkinTonePicker.swift */; }; 7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */; }; 7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */; }; 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */; }; 7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; }; 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; }; 7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 7B50D64C28AC7CF80086CCEC /* silence.aiff */; }; + 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; }; + 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; }; 7B7CB189270430D20079FF93 /* CallMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB188270430D20079FF93 /* CallMessageView.swift */; }; 7B7CB18B270591630079FF93 /* ShareLogsModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */; }; 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; }; 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */; }; 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; }; + 7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682228A4C1210069F315 /* UpdateTypes.swift */; }; + 7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */; }; + 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682928B6F1420069F315 /* ReactionResponse.swift */; }; + 7B81682C28B72F480069F315 /* PendingChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682B28B72F480069F315 /* PendingChange.swift */; }; + 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */; }; 7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */; }; 7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */; }; 7B93D07127CF194000811CB6 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */; }; 7B93D07727CF1A8A00811CB6 /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */; }; + 7B9F71C928470667006DFE7B /* ReactionListSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71C828470667006DFE7B /* ReactionListSheet.swift */; }; + 7B9F71D02852EEE2006DFE7B /* Emoji+Category.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71CB2852EEE2006DFE7B /* Emoji+Category.swift */; }; + 7B9F71D12852EEE2006DFE7B /* EmojiWithSkinTones+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71CC2852EEE2006DFE7B /* EmojiWithSkinTones+String.swift */; }; + 7B9F71D22852EEE2006DFE7B /* Emoji+SkinTones.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71CD2852EEE2006DFE7B /* Emoji+SkinTones.swift */; }; + 7B9F71D32852EEE2006DFE7B /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71CE2852EEE2006DFE7B /* Emoji.swift */; }; + 7B9F71D42852EEE2006DFE7B /* Emoji+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71CF2852EEE2006DFE7B /* Emoji+Name.swift */; }; + 7B9F71D72853100A006DFE7B /* Emoji+Available.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71D528531009006DFE7B /* Emoji+Available.swift */; }; + 7B9F71D82853100A006DFE7B /* EmojiWithSkinTones.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71D628531009006DFE7B /* EmojiWithSkinTones.swift */; }; 7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA68908272A27BE00EFC32F /* SessionCall.swift */; }; 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */; }; 7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */; }; @@ -147,6 +165,7 @@ 7BAF54D427ACCF01003D12F8 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */; }; 7BAF54D827ACD0E3003D12F8 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */; }; 7BAF54DC27ACD12B003D12F8 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */; }; + 7BBBDC44286EAD2D00747E59 /* TappableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */; }; 7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */; }; 7BC01A42241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7BC707F227290ACB002817AD /* SessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC707F127290ACB002817AD /* SessionCallManager.swift */; }; @@ -155,6 +174,7 @@ 7BD477B027F526FF004E2822 /* BlockListUIUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD477AF27F526FF004E2822 /* BlockListUIUtils.swift */; }; 7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */; }; 7BDCFC0B2421EB7600641C39 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; }; + 7BFA8AE32831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFA8AE22831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift */; }; 7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD1A892745C4F000FB91B9 /* Permissions.swift */; }; 7BFD1A8C2747150E00FB91B9 /* TurnServerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD1A8B2747150E00FB91B9 /* TurnServerInfo.swift */; }; 7BFD1A972747689000FB91B9 /* Session-Turn-Server in Resources */ = {isa = PBXBuildFile; fileRef = 7BFD1A962747689000FB91B9 /* Session-Turn-Server */; }; @@ -181,7 +201,6 @@ B81D25C526157F40004D1FE1 /* storage-seed-1.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B726157F20004D1FE1 /* storage-seed-1.crt */; }; B81D25C626157F40004D1FE1 /* public-loki-foundation.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B826157F20004D1FE1 /* public-loki-foundation.crt */; }; B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821494525D4D6FF009C0F2A /* URLModal.swift */; }; - B821494F25D4E163009C0F2A /* BodyTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821494E25D4E163009C0F2A /* BodyTextView.swift */; }; B82149B825D60393009C0F2A /* BlockedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82149B725D60393009C0F2A /* BlockedModal.swift */; }; B82149C125D605C6009C0F2A /* InfoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82149C025D605C6009C0F2A /* InfoBanner.swift */; }; B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D2825C7A4B400488AB4 /* InputView.swift */; }; @@ -303,8 +322,6 @@ C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAFD255A580600E217F9 /* LRUCache.swift */; }; C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB68255A580F00E217F9 /* ContentProxy.swift */; }; C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */; }; - C32C5FBB256E0206003C73A2 /* OWSBackgroundTask.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */; }; - C32C5FC4256E0209003C73A2 /* OWSBackgroundTask.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */; settings = {ATTRIBUTES = (Public, ); }; }; C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */; }; C32C6018256E07F9003C73A2 /* NSUserDefaults+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; }; C33100082558FF6D00070591 /* NewConversationButtonSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83F2B85240C7B8F000A54AB /* NewConversationButtonSet.swift */; }; @@ -586,6 +603,9 @@ FD09799727FFA84A00936362 /* RecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799627FFA84900936362 /* RecipientState.swift */; }; FD09799927FFC1A300936362 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799827FFC1A300936362 /* Attachment.swift */; }; FD09799B27FFC82D00936362 /* Quote.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799A27FFC82D00936362 /* Quote.swift */; }; + FD09B7E328865FDA00ED0B66 /* HighlightMentionBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E228865FDA00ED0B66 /* HighlightMentionBackgroundView.swift */; }; + FD09B7E5288670BB00ED0B66 /* _008_EmojiReacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */; }; + FD09B7E7288670FD00ED0B66 /* Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E6288670FD00ED0B66 /* Reaction.swift */; }; FD09C5E2282212B3000CE219 /* JobDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E1282212B3000CE219 /* JobDependencies.swift */; }; FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */; }; FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E728264937000CE219 /* MediaDetailViewController.swift */; }; @@ -662,6 +682,8 @@ FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */; }; FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */; }; FD4B200E283492210034334B /* InsetLockableTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4B200D283492210034334B /* InsetLockableTableView.swift */; }; + FD52090028AF6153006098F6 /* OWSBackgroundTask.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */; }; + FD52090128AF61BA006098F6 /* OWSBackgroundTask.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */; settings = {ATTRIBUTES = (Public, ); }; }; FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */; }; FD5C72F9284F0E880029977D /* MessageReceiver+TypingIndicators.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */; }; FD5C72FB284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */; }; @@ -781,12 +803,10 @@ FDCDB8DE2810F73B00352A0C /* Differentiable+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */; }; FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */; }; FDCDB8E42817819600352A0C /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDCDB8F12817ABE600352A0C /* Optional+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8F02817ABE600352A0C /* Optional+Utilities.swift */; }; FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; }; FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; }; FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; }; FDE72118286C156E0093DF33 /* ChatSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE72117286C156E0093DF33 /* ChatSettingsViewController.swift */; }; - FDE72154287FE4470093DF33 /* HighlightMentionBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE72153287FE4470093DF33 /* HighlightMentionBackgroundView.swift */; }; FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */; }; FDED2E3C282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDED2E3B282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift */; }; FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */; }; @@ -1143,6 +1163,9 @@ 7B1581E3271FC59C00848B49 /* CallModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallModal.swift; sourceTree = ""; }; 7B1581E5271FD2A100848B49 /* VideoPreviewVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPreviewVC.swift; sourceTree = ""; }; 7B1581E727210ECC00848B49 /* RenderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderView.swift; sourceTree = ""; }; + 7B1B52D728580C6D006069F2 /* EmojiPickerSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerSheet.swift; sourceTree = ""; }; + 7B1B52DB28580D50006069F2 /* EmojiPickerCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerCollectionView.swift; sourceTree = ""; }; + 7B1B52DC28580D50006069F2 /* EmojiSkinTonePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiSkinTonePicker.swift; sourceTree = ""; }; 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSENotificationPresenter.swift; sourceTree = ""; }; 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Timeout.swift"; sourceTree = ""; }; 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = ""; }; @@ -1150,15 +1173,30 @@ 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = ""; }; 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = ""; }; 7B50D64C28AC7CF80086CCEC /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = ""; }; + 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = ""; }; + 7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; 7B7CB188270430D20079FF93 /* CallMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageView.swift; sourceTree = ""; }; 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLogsModal.swift; sourceTree = ""; }; 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = ""; }; 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniCallView.swift; sourceTree = ""; }; 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = ""; }; + 7B81682228A4C1210069F315 /* UpdateTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTypes.swift; sourceTree = ""; }; + 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = ""; }; + 7B81682928B6F1420069F315 /* ReactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionResponse.swift; sourceTree = ""; }; + 7B81682B28B72F480069F315 /* PendingChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingChange.swift; sourceTree = ""; }; + 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Reaction.swift"; sourceTree = ""; }; 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = ""; }; 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = ""; }; 7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = ""; }; 7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockDataGenerator.swift; sourceTree = ""; }; + 7B9F71C828470667006DFE7B /* ReactionListSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionListSheet.swift; sourceTree = ""; }; + 7B9F71CB2852EEE2006DFE7B /* Emoji+Category.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Emoji+Category.swift"; sourceTree = ""; }; + 7B9F71CC2852EEE2006DFE7B /* EmojiWithSkinTones+String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EmojiWithSkinTones+String.swift"; sourceTree = ""; }; + 7B9F71CD2852EEE2006DFE7B /* Emoji+SkinTones.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Emoji+SkinTones.swift"; sourceTree = ""; }; + 7B9F71CE2852EEE2006DFE7B /* Emoji.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = ""; }; + 7B9F71CF2852EEE2006DFE7B /* Emoji+Name.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Emoji+Name.swift"; sourceTree = ""; }; + 7B9F71D528531009006DFE7B /* Emoji+Available.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Emoji+Available.swift"; sourceTree = ""; }; + 7B9F71D628531009006DFE7B /* EmojiWithSkinTones.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiWithSkinTones.swift; sourceTree = ""; }; 7BA68908272A27BE00EFC32F /* SessionCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCall.swift; sourceTree = ""; }; 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXCallController.swift"; sourceTree = ""; }; 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXProvider.swift"; sourceTree = ""; }; @@ -1170,6 +1208,7 @@ 7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = ""; }; 7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; 7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; + 7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TappableLabel.swift; sourceTree = ""; }; 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; 7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1180,6 +1219,7 @@ 7BD477AF27F526FF004E2822 /* BlockListUIUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockListUIUtils.swift; sourceTree = ""; }; 7BDCFC0424206E7300641C39 /* SessionNotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SessionNotificationServiceExtension.entitlements; sourceTree = ""; }; 7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtensionContext.swift; sourceTree = ""; }; + 7BFA8AE22831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContextMenuVC+EmojiReactsView.swift"; sourceTree = ""; }; 7BFD1A892745C4F000FB91B9 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = ""; }; 7BFD1A8B2747150E00FB91B9 /* TurnServerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnServerInfo.swift; sourceTree = ""; }; 7BFD1A962747689000FB91B9 /* Session-Turn-Server */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "Session-Turn-Server"; sourceTree = ""; }; @@ -1215,7 +1255,6 @@ B81D25B826157F20004D1FE1 /* public-loki-foundation.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "public-loki-foundation.crt"; sourceTree = ""; }; B81D25B926157F20004D1FE1 /* storage-seed-3.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "storage-seed-3.crt"; sourceTree = ""; }; B821494525D4D6FF009C0F2A /* URLModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLModal.swift; sourceTree = ""; }; - B821494E25D4E163009C0F2A /* BodyTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BodyTextView.swift; sourceTree = ""; }; B82149B725D60393009C0F2A /* BlockedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedModal.swift; sourceTree = ""; }; B82149C025D605C6009C0F2A /* InfoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoBanner.swift; sourceTree = ""; }; B8269D2825C7A4B400488AB4 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = ""; }; @@ -1654,6 +1693,9 @@ FD09799627FFA84900936362 /* RecipientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientState.swift; sourceTree = ""; }; FD09799827FFC1A300936362 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; FD09799A27FFC82D00936362 /* Quote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quote.swift; sourceTree = ""; }; + FD09B7E228865FDA00ED0B66 /* HighlightMentionBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightMentionBackgroundView.swift; sourceTree = ""; }; + FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _008_EmojiReacts.swift; sourceTree = ""; }; + FD09B7E6288670FD00ED0B66 /* Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reaction.swift; sourceTree = ""; }; FD09C5E1282212B3000CE219 /* JobDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobDependencies.swift; sourceTree = ""; }; FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryViewModel.swift; sourceTree = ""; }; FD09C5E728264937000CE219 /* MediaDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDetailViewController.swift; sourceTree = ""; }; @@ -1693,6 +1735,7 @@ FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_FixHiddenModAdminSupport.swift; sourceTree = ""; }; FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlteration.swift; sourceTree = ""; }; FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitySpec.swift; sourceTree = ""; }; + FD37EA1A28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = ""; }; FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = ""; }; FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestInfoSpec.swift; sourceTree = ""; }; FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = ""; }; @@ -1818,14 +1861,12 @@ FDC6D75F2862B3F600B04575 /* Dependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependencies.swift; sourceTree = ""; }; FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Differentiable+Utilities.swift"; sourceTree = ""; }; FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; - FDCDB8F02817ABE600352A0C /* Optional+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Utilities.swift"; sourceTree = ""; }; FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Utilities.swift"; sourceTree = ""; }; FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = ""; }; FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = ""; }; FDE72117286C156E0093DF33 /* ChatSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSettingsViewController.swift; sourceTree = ""; }; FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = ""; }; FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LintLocalizableStrings.swift; sourceTree = ""; }; - FDE72153287FE4470093DF33 /* HighlightMentionBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightMentionBackgroundView.swift; sourceTree = ""; }; FDE77F68280F9EDA002CFC5D /* JobRunnerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerError.swift; sourceTree = ""; }; FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlMessageProcessRecord.swift; sourceTree = ""; }; FDED2E3B282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+ReusableView.swift"; sourceTree = ""; }; @@ -2139,6 +2180,16 @@ path = Utilities; sourceTree = ""; }; + 7B1B52BD2851ADE1006069F2 /* Emoji Picker */ = { + isa = PBXGroup; + children = ( + 7B1B52DB28580D50006069F2 /* EmojiPickerCollectionView.swift */, + 7B1B52DC28580D50006069F2 /* EmojiSkinTonePicker.swift */, + 7B1B52D728580C6D006069F2 /* EmojiPickerSheet.swift */, + ); + path = "Emoji Picker"; + sourceTree = ""; + }; 7B7CB18C270D06350079FF93 /* Views & Modals */ = { isa = PBXGroup; children = ( @@ -2151,6 +2202,14 @@ path = "Views & Modals"; sourceTree = ""; }; + 7B81682428B30BEC0069F315 /* Recovered References */ = { + isa = PBXGroup; + children = ( + FD37EA1A28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift */, + ); + name = "Recovered References"; + sourceTree = ""; + }; 7B93D06827CF173D00811CB6 /* Message Requests */ = { isa = PBXGroup; children = ( @@ -2161,6 +2220,20 @@ path = "Message Requests"; sourceTree = ""; }; + 7B9F71CA2852EEE2006DFE7B /* Emoji */ = { + isa = PBXGroup; + children = ( + 7B9F71D528531009006DFE7B /* Emoji+Available.swift */, + 7B9F71D628531009006DFE7B /* EmojiWithSkinTones.swift */, + 7B9F71CB2852EEE2006DFE7B /* Emoji+Category.swift */, + 7B9F71CC2852EEE2006DFE7B /* EmojiWithSkinTones+String.swift */, + 7B9F71CD2852EEE2006DFE7B /* Emoji+SkinTones.swift */, + 7B9F71CE2852EEE2006DFE7B /* Emoji.swift */, + 7B9F71CF2852EEE2006DFE7B /* Emoji+Name.swift */, + ); + path = Emoji; + sourceTree = ""; + }; 7BA68907272A279900EFC32F /* Call Management */ = { isa = PBXGroup; children = ( @@ -2226,6 +2299,8 @@ B8EB20EF2640F7F000773E52 /* OpenGroupInvitationView.swift */, 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */, 7B7CB188270430D20079FF93 /* CallMessageView.swift */, + 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */, + 7B7037442834BCC0000DCF35 /* ReactionView.swift */, ); path = "Content Views"; sourceTree = ""; @@ -2252,7 +2327,6 @@ B821494525D4D6FF009C0F2A /* URLModal.swift */, B8AF4BB326A5204600583500 /* SendSeedModal.swift */, B8758859264503A6000E60D0 /* JoinOpenGroupModal.swift */, - B821494E25D4E163009C0F2A /* BodyTextView.swift */, B82149B725D60393009C0F2A /* BlockedModal.swift */, C374EEE125DA26740073A857 /* LinkPreviewModal.swift */, B82149C025D605C6009C0F2A /* InfoBanner.swift */, @@ -2261,6 +2335,7 @@ FD4B200D283492210034334B /* InsetLockableTableView.swift */, 7B1581E3271FC59C00848B49 /* CallModal.swift */, 7BFFB33B27D02F5800BEA04E /* CallPermissionRequestModal.swift */, + 7B9F71C828470667006DFE7B /* ReactionListSheet.swift */, ); path = "Views & Modals"; sourceTree = ""; @@ -2272,6 +2347,7 @@ B835247725C38D190089A44F /* Message Cells */, C328252E25CA54F70062D0A7 /* Context Menu */, B821493625D4D6A7009C0F2A /* Views & Modals */, + 7B1B52BD2851ADE1006069F2 /* Emoji Picker */, C302094625DCDFD3001F572D /* Settings */, FDF222062818CECF000A4995 /* ConversationViewModel.swift */, B835246D25C38ABF0089A44F /* ConversationVC.swift */, @@ -2448,12 +2524,10 @@ B8CCF63B239757C10091D419 /* Shared */ = { isa = PBXGroup; children = ( - FD4B200A283367350034334B /* Models */, 4CA46F4B219CCC630038ABDE /* CaptionView.swift */, 4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */, 45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */, 34386A53207D271C009F5D9C /* NeverClearView.swift */, - FDE72153287FE4470093DF33 /* HighlightMentionBackgroundView.swift */, 34F308A01ECB469700BB7697 /* OWSBezierPathView.h */, 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */, 34330AA11E79686200DF2FB9 /* OWSProgressView.h */, @@ -2514,6 +2588,7 @@ isa = PBXGroup; children = ( C3C2A74C2553A39700C340D1 /* VisibleMessage.swift */, + 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */, C379DCF3256735770002D4EB /* VisibleMessage+Attachment.swift */, C3C2A7552553A3AB00C340D1 /* VisibleMessage+Quote.swift */, C3C2A75E2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift */, @@ -2602,6 +2677,7 @@ C328253F25CA55880062D0A7 /* ContextMenuVC.swift */, C328254825CA60E60062D0A7 /* ContextMenuVC+Action.swift */, C328255125CA64470062D0A7 /* ContextMenuVC+ActionView.swift */, + 7BFA8AE22831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift */, ); path = "Context Menu"; sourceTree = ""; @@ -2714,6 +2790,8 @@ B8CCF638239721E20091D419 /* TabBar.swift */, B8BB82B423947F2D00BA5194 /* TextField.swift */, C3C3CF8824D8EED300E1CCE7 /* TextView.swift */, + 7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */, + FD09B7E228865FDA00ED0B66 /* HighlightMentionBackgroundView.swift */, ); path = Components; sourceTree = ""; @@ -3060,8 +3138,6 @@ C38EF2F5255B6DBC007E1867 /* OWSAudioPlayer.h */, C38EF2F7255B6DBC007E1867 /* OWSAudioPlayer.m */, C38EF281255B6D84007E1867 /* OWSAudioSession.swift */, - C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */, - C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */, FDF0B75D280AAF35004C14C5 /* Preferences.swift */, C38EF2FB255B6DBD007E1867 /* OWSWindowManager.h */, C38EF306255B6DBE007E1867 /* OWSWindowManager.m */, @@ -3129,7 +3205,6 @@ B8A582AE258C65D000AFD84C /* Networking */, B8A582AD258C655E00AFD84C /* PromiseKit */, FD09796527F6B0A800936362 /* Utilities */, - FDCDB8EF2817ABCE00352A0C /* Utilities */, C3D9E43025676D3D0040E4F3 /* Configuration.swift */, ); path = SessionUtilitiesKit; @@ -3326,6 +3401,7 @@ D221A08C169C9E5E00537ABF /* Frameworks */, D221A08A169C9E5E00537ABF /* Products */, 2BADBA206E0B8D297E313FBA /* Pods */, + 7B81682428B30BEC0069F315 /* Recovered References */, ); sourceTree = ""; }; @@ -3402,6 +3478,7 @@ isa = PBXGroup; children = ( C3F0A58F255C8E3D007BE2A3 /* Meta */, + 7B9F71CA2852EEE2006DFE7B /* Emoji */, B8B558ED26C4B55F00693325 /* Calls */, C360969C25AD18BA008B62B2 /* Closed Groups */, B835246C25C38AA20089A44F /* Conversations */, @@ -3429,6 +3506,8 @@ FD09797127FAA2F500936362 /* Optional+Utilities.swift */, FD09797C27FBDB2000936362 /* Notification+Utilities.swift */, FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */, + C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */, + C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */, ); path = Utilities; sourceTree = ""; @@ -3454,6 +3533,7 @@ FD09C5E9282A1BB2000CE219 /* ThreadTypingIndicator.swift */, FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */, FD5C7308285007920029977D /* BlindedIdLookup.swift */, + FD09B7E6288670FD00ED0B66 /* Reaction.swift */, ); path = Models; sourceTree = ""; @@ -3467,6 +3547,8 @@ FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */, FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */, FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */, + 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */, + FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */, ); path = Migrations; sourceTree = ""; @@ -3650,13 +3732,6 @@ path = "Shared Models"; sourceTree = ""; }; - FD4B200A283367350034334B /* Models */ = { - isa = PBXGroup; - children = ( - ); - path = Models; - sourceTree = ""; - }; FD716E6F28505E5100C96BF4 /* Views */ = { isa = PBXGroup; children = ( @@ -3785,6 +3860,8 @@ FDC438A327BB107F00C60D73 /* UserBanRequest.swift */, FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */, FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */, + 7B81682928B6F1420069F315 /* ReactionResponse.swift */, + 7B81682B28B72F480069F315 /* PendingChange.swift */, ); path = Models; sourceTree = ""; @@ -3812,6 +3889,7 @@ FDC4384E27B4804F00C60D73 /* Header.swift */, FDC4385027B4807400C60D73 /* QueryParam.swift */, FD83B9CD27D17A04005E1583 /* Request.swift */, + 7B81682228A4C1210069F315 /* UpdateTypes.swift */, ); path = "Common Networking"; sourceTree = ""; @@ -3887,14 +3965,6 @@ path = Models; sourceTree = ""; }; - FDCDB8EF2817ABCE00352A0C /* Utilities */ = { - isa = PBXGroup; - children = ( - FDCDB8F02817ABE600352A0C /* Optional+Utilities.swift */, - ); - path = Utilities; - sourceTree = ""; - }; FDE7214E287E50D50093DF33 /* Scripts */ = { isa = PBXGroup; children = ( @@ -4009,6 +4079,7 @@ C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */, C32C6018256E07F9003C73A2 /* NSUserDefaults+OWS.h in Headers */, B8856D8D256F1502001CE70E /* UIView+OWS.h in Headers */, + FD52090128AF61BA006098F6 /* OWSBackgroundTask.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4018,7 +4089,6 @@ files = ( C3C2A6F425539DE700C340D1 /* SessionMessagingKit.h in Headers */, C32C5C46256DCBB2003C73A2 /* AppReadiness.h in Headers */, - C32C5FC4256E0209003C73A2 /* OWSBackgroundTask.h in Headers */, FD716E732850647900C96BF4 /* NSData+messagePadding.h in Headers */, B8856D72256F1421001CE70E /* OWSWindowManager.h in Headers */, B8856CF7256F105E001CE70E /* OWSAudioPlayer.h in Headers */, @@ -4853,6 +4923,8 @@ buildActionMask = 2147483647; files = ( C331FF972558FA6B00070591 /* Fonts.swift in Sources */, + 7BBBDC44286EAD2D00747E59 /* TappableLabel.swift in Sources */, + FD09B7E328865FDA00ED0B66 /* HighlightMentionBackgroundView.swift in Sources */, C331FF9B2558FA6B00070591 /* Gradients.swift in Sources */, C331FFB82558FA8D00070591 /* DeviceUtilities.swift in Sources */, C331FFE72558FB0000070591 /* TextField.swift in Sources */, @@ -5026,7 +5098,6 @@ FDA8EB10280F8238002B68E5 /* Codable+Utilities.swift in Sources */, C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */, FD09797B27FBB25900936362 /* Updatable.swift in Sources */, - FDCDB8F12817ABE600352A0C /* Optional+Utilities.swift in Sources */, 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */, C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */, FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */, @@ -5072,6 +5143,7 @@ FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */, FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */, 7B0EFDEE274F598600FFAAE7 /* TimestampUtils.swift in Sources */, + FD52090028AF6153006098F6 /* OWSBackgroundTask.m in Sources */, C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */, C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */, B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */, @@ -5116,6 +5188,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */, FD86585828507B24008B6CF9 /* NSData+messagePadding.m in Sources */, FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */, B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */, @@ -5127,18 +5200,21 @@ C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */, FDF40CDE2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift in Sources */, FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */, + 7B81682C28B72F480069F315 /* PendingChange.swift in Sources */, FD77289A284AF1BD0018502F /* Sodium+Utilities.swift in Sources */, FD5C7309285007920029977D /* BlindedIdLookup.swift in Sources */, 7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */, C300A5F22554B09800555489 /* MessageSender.swift in Sources */, B8B558FF26C4E05E00693325 /* WebRTCSession+MessageHandling.swift in Sources */, C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */, + FD09B7E7288670FD00ED0B66 /* Reaction.swift in Sources */, FD245C58285065F700B966DD /* OpenGroupServerIdLookup.swift in Sources */, FD245C5A2850660100B966DD /* LinkPreviewDraft.swift in Sources */, FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */, FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */, FD716E6A2850327900C96BF4 /* EndCallMode.swift in Sources */, FDF0B75C2807F41D004C14C5 /* MessageSender+Convenience.swift in Sources */, + 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */, FD09799727FFA84A00936362 /* RecipientState.swift in Sources */, FDA8EB00280E8D58002B68E5 /* FailedAttachmentDownloadsJob.swift in Sources */, FD09798927FD1C5A00936362 /* OpenGroup.swift in Sources */, @@ -5151,13 +5227,19 @@ FD245C6A2850666F00B966DD /* FileServerAPI.swift in Sources */, FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */, FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */, + FD09B7E5288670BB00ED0B66 /* _008_EmojiReacts.swift in Sources */, FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */, 7BFD1A8C2747150E00FB91B9 /* TurnServerInfo.swift in Sources */, + C3D9E3BF25676AD70040E4F3 /* (null) in Sources */, + B8BF43BA26CC95FB007828D1 /* WebRTC+Utilities.swift in Sources */, + 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */, + C3BBE0B52554F0E10050F1E3 /* (null) in Sources */, FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */, FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */, FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */, C3D9E3BF25676AD70040E4F3 /* (null) in Sources */, B8BF43BA26CC95FB007828D1 /* WebRTC+Utilities.swift in Sources */, + 7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */, FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */, C3BBE0B52554F0E10050F1E3 /* (null) in Sources */, FD5C72FB284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift in Sources */, @@ -5245,7 +5327,6 @@ FD245C5C2850660A00B966DD /* ConfigurationMessage.swift in Sources */, FD5C7301284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift in Sources */, C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */, - C32C5FBB256E0206003C73A2 /* OWSBackgroundTask.m in Sources */, FD245C642850664F00B966DD /* Threading.swift in Sources */, FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */, C32C5C3D256DCBAF003C73A2 /* AppReadiness.m in Sources */, @@ -5291,6 +5372,7 @@ FDFDE12A282D056B0098B17F /* MediaZoomAnimationController.swift in Sources */, 4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */, 34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */, + 7BFA8AE32831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift in Sources */, 7B13E1EB2811138200BD4F64 /* PrivacySettingsTableViewController.swift in Sources */, C3E5C2FA251DBABB0040DFFC /* EditClosedGroupVC.swift in Sources */, 7BAF54D027ACCEEC003D12F8 /* EmptySearchResultCell.swift in Sources */, @@ -5310,6 +5392,7 @@ EF764C351DB67CC5000D9A87 /* UIViewController+Permissions.m in Sources */, 45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */, B83524A525C3BA4B0089A44F /* InfoMessageCell.swift in Sources */, + 7B9F71D82853100A006DFE7B /* EmojiWithSkinTones.swift in Sources */, FDE72118286C156E0093DF33 /* ChatSettingsViewController.swift in Sources */, B84A89BC25DE328A0040017D /* ProfilePictureVC.swift in Sources */, 34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */, @@ -5319,9 +5402,9 @@ 7B1581E6271FD2A100848B49 /* VideoPreviewVC.swift in Sources */, B83F2B88240CB75A000A54AB /* UIImage+Scaling.swift in Sources */, 3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */, - FDE72154287FE4470093DF33 /* HighlightMentionBackgroundView.swift in Sources */, 340FC8AA204DAC8D007AEB0F /* NotificationSettingsViewController.m in Sources */, 4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */, + 7B9F71D12852EEE2006DFE7B /* EmojiWithSkinTones+String.swift in Sources */, 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */, B886B4A92398BA1500211ABE /* QRCode.swift in Sources */, 3496955D219B605E00DCFE74 /* PhotoCollectionPickerController.swift in Sources */, @@ -5333,7 +5416,9 @@ 3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */, C3A76A8D25DB83F90074CB90 /* PermissionMissingModal.swift in Sources */, 340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */, + 7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */, B849789625D4A2F500D0D0B3 /* LinkPreviewView.swift in Sources */, + 7B1B52DF28580D51006069F2 /* EmojiPickerCollectionView.swift in Sources */, C3D0972B2510499C00F6E3E4 /* BackgroundPoller.swift in Sources */, C3548F0624456447009433A8 /* PNModeVC.swift in Sources */, B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */, @@ -5361,6 +5446,7 @@ 45A6DAD61EBBF85500893231 /* ReminderView.swift in Sources */, B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */, B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */, + 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */, B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */, B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */, B877E24226CA12910007970A /* CallVC.swift in Sources */, @@ -5388,11 +5474,14 @@ 4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */, B82149C125D605C6009C0F2A /* InfoBanner.swift in Sources */, C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */, + 7B9F71D02852EEE2006DFE7B /* Emoji+Category.swift in Sources */, 7BAADFCC27B0EF23007BCF92 /* CallVideoView.swift in Sources */, B8CCF63F23975CFB0091D419 /* JoinOpenGroupVC.swift in Sources */, FD848B9828422F1A000E298B /* Date+Utilities.swift in Sources */, B85357C323A1BD1200AAF6CD /* SeedVC.swift in Sources */, 45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */, + 7B9F71C928470667006DFE7B /* ReactionListSheet.swift in Sources */, + 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */, B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */, 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */, B875885A264503A6000E60D0 /* JoinOpenGroupModal.swift in Sources */, @@ -5408,14 +5497,17 @@ B8041A9525C8FA1D003C2166 /* MediaLoaderView.swift in Sources */, 45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */, 34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */, + 7B9F71D42852EEE2006DFE7B /* Emoji+Name.swift in Sources */, 4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */, C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */, 340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */, 7B1581E4271FC59D00848B49 /* CallModal.swift in Sources */, 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */, B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */, + 7B9F71D72853100A006DFE7B /* Emoji+Available.swift in Sources */, FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */, 7B7CB189270430D20079FF93 /* CallMessageView.swift in Sources */, + 7B9F71D32852EEE2006DFE7B /* Emoji.swift in Sources */, C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */, B82B4090239DD75000A248E7 /* RestoreVC.swift in Sources */, 3488F9362191CC4000E524CC /* MediaView.swift in Sources */, @@ -5436,11 +5528,11 @@ C31A6C5A247F214E001123EF /* UIView+Glow.swift in Sources */, 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */, 340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */, + 7B9F71D22852EEE2006DFE7B /* Emoji+SkinTones.swift in Sources */, 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */, B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */, 7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */, B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */, - B821494F25D4E163009C0F2A /* BodyTextView.swift in Sources */, FD716E6C28505E1C00C96BF4 /* MessageRequestsViewModel.swift in Sources */, C35E8AAE2485E51D00ACB629 /* IP2Country.swift in Sources */, B835249B25C3AB650089A44F /* VisibleMessageCell.swift in Sources */, @@ -5448,6 +5540,7 @@ B8D0A25025E3678700C1835E /* LinkDeviceVC.swift in Sources */, B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */, 7B93D07727CF1A8A00811CB6 /* MockDataGenerator.swift in Sources */, + 7B1B52D828580C6D006069F2 /* EmojiPickerSheet.swift in Sources */, 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */, FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */, FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */, @@ -5703,7 +5796,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 374; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5728,7 +5821,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.13.0; + MARKETING_VERSION = 2.1.0; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5776,7 +5869,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 374; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -5806,7 +5899,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.13.0; + MARKETING_VERSION = 2.1.0; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5842,7 +5935,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 374; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5865,7 +5958,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.13.0; + MARKETING_VERSION = 2.1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -5916,7 +6009,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 374; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -5944,7 +6037,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.13.0; + MARKETING_VERSION = 2.1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -6854,7 +6947,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 369; + CURRENT_PROJECT_VERSION = 374; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6893,7 +6986,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.0.2; + MARKETING_VERSION = 2.1.0; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -6926,7 +7019,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 369; + CURRENT_PROJECT_VERSION = 374; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6965,7 +7058,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.0.2; + MARKETING_VERSION = 2.1.0; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index 13b3b8e0c..cbe87d1b0 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -7,75 +7,107 @@ extension ContextMenuVC { struct Action { let icon: UIImage? let title: String + let isEmojiAction: Bool + let isEmojiPlus: Bool let isDismissAction: Bool let work: () -> Void + + // MARK: - Initialization + + init( + icon: UIImage? = nil, + title: String = "", + isEmojiAction: Bool = false, + isEmojiPlus: Bool = false, + isDismissAction: Bool = false, + work: @escaping () -> Void + ) { + self.icon = icon + self.title = title + self.isEmojiAction = isEmojiAction + self.isEmojiPlus = isEmojiPlus + self.isDismissAction = isDismissAction + self.work = work + } + + // MARK: - Actions static func reply(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(named: "ic_reply"), - title: "context_menu_reply".localized(), - isDismissAction: false + title: "context_menu_reply".localized() ) { delegate?.reply(cellViewModel) } } static func copy(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(named: "ic_copy"), - title: "copy".localized(), - isDismissAction: false + title: "copy".localized() ) { delegate?.copy(cellViewModel) } } static func copySessionID(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(named: "ic_copy"), - title: "vc_conversation_settings_copy_session_id_button_title".localized(), - isDismissAction: false + title: "vc_conversation_settings_copy_session_id_button_title".localized() ) { delegate?.copySessionID(cellViewModel) } } static func delete(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(named: "ic_trash"), - title: "TXT_DELETE_TITLE".localized(), - isDismissAction: false + title: "TXT_DELETE_TITLE".localized() ) { delegate?.delete(cellViewModel) } } static func save(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(named: "ic_download"), - title: "context_menu_save".localized(), - isDismissAction: false + title: "context_menu_save".localized() ) { delegate?.save(cellViewModel) } } static func ban(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(named: "ic_block"), - title: "context_menu_ban_user".localized(), - isDismissAction: false + title: "context_menu_ban_user".localized() ) { delegate?.ban(cellViewModel) } } static func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(named: "ic_block"), - title: "context_menu_ban_and_delete_all".localized(), - isDismissAction: false + title: "context_menu_ban_and_delete_all".localized() ) { delegate?.banAndDeleteAllMessages(cellViewModel) } } + static func react(_ cellViewModel: MessageViewModel, _ emoji: EmojiWithSkinTones, _ delegate: ContextMenuActionDelegate?) -> Action { + return Action( + title: emoji.rawValue, + isEmojiAction: true + ) { delegate?.react(cellViewModel, with: emoji) } + } + + static func emojiPlusButton(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { + return Action( + isEmojiPlus: true + ) { delegate?.showFullEmojiKeyboard(cellViewModel) } + } + static func dismiss(_ delegate: ContextMenuActionDelegate?) -> Action { return Action( - icon: nil, - title: "", isDismissAction: true ) { delegate?.contextMenuDismissed() } } } - static func actions(for cellViewModel: MessageViewModel, currentUserIsOpenGroupModerator: Bool, delegate: ContextMenuActionDelegate?) -> [Action]? { + static func actions( + for cellViewModel: MessageViewModel, + recentEmojis: [EmojiWithSkinTones], + currentUserIsOpenGroupModerator: Bool, + currentThreadIsMessageRequest: Bool, + delegate: ContextMenuActionDelegate? + ) -> [Action]? { // No context items for info messages guard cellViewModel.variant == .standardOutgoing || cellViewModel.variant == .standardIncoming else { return nil @@ -118,13 +150,21 @@ extension ContextMenuVC { ) let canDelete: Bool = ( cellViewModel.threadVariant != .openGroup || - currentUserIsOpenGroupModerator + currentUserIsOpenGroupModerator || + cellViewModel.state == .failed ) let canBan: Bool = ( cellViewModel.threadVariant == .openGroup && currentUserIsOpenGroupModerator ) + let shouldShowEmojiActions: Bool = { + if cellViewModel.threadVariant == .openGroup { + return OpenGroupManager.isOpenGroupSupport(.reactions, on: cellViewModel.threadOpenGroupServer) + } + return !currentThreadIsMessageRequest + }() + let generatedActions: [Action] = [ (canReply ? Action.reply(cellViewModel, delegate) : nil), (canCopy ? Action.copy(cellViewModel, delegate) : nil), @@ -132,8 +172,10 @@ extension ContextMenuVC { (canCopySessionId ? Action.copySessionID(cellViewModel, delegate) : nil), (canDelete ? Action.delete(cellViewModel, delegate) : nil), (canBan ? Action.ban(cellViewModel, delegate) : nil), - (canBan ? Action.banAndDeleteAllMessages(cellViewModel, delegate) : nil) + (canBan ? Action.banAndDeleteAllMessages(cellViewModel, delegate) : nil), ] + .appending(contentsOf: (shouldShowEmojiActions ? recentEmojis : []).map { Action.react(cellViewModel, $0, delegate) }) + .appending(Action.emojiPlusButton(cellViewModel, delegate)) .compactMap { $0 } guard !generatedActions.isEmpty else { return [] } @@ -152,5 +194,7 @@ protocol ContextMenuActionDelegate { func save(_ cellViewModel: MessageViewModel) func ban(_ cellViewModel: MessageViewModel) func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel) + func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones) + func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel) func contextMenuDismissed() } diff --git a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift new file mode 100644 index 000000000..6ba658182 --- /dev/null +++ b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift @@ -0,0 +1,113 @@ +// Copyright Š 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUIKit +import SessionUtilitiesKit + +extension ContextMenuVC { + final class EmojiReactsView: UIView { + private let action: Action + private let dismiss: () -> Void + + // MARK: - Settings + + private static let size: CGFloat = 40 + + // MARK: - Lifecycle + + init(for action: Action, dismiss: @escaping () -> Void) { + self.action = action + self.dismiss = dismiss + + super.init(frame: CGRect.zero) + + setUpViewHierarchy() + } + + override init(frame: CGRect) { + preconditionFailure("Use init(for:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(for:) instead.") + } + + private func setUpViewHierarchy() { + let emojiLabel = UILabel() + emojiLabel.text = self.action.title + emojiLabel.font = .systemFont(ofSize: Values.veryLargeFontSize) + emojiLabel.set(.height, to: ContextMenuVC.EmojiReactsView.size) + addSubview(emojiLabel) + emojiLabel.pin(to: self) + + // Tap gesture recognizer + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + addGestureRecognizer(tapGestureRecognizer) + } + + // MARK: - Interaction + + @objc private func handleTap() { + action.work() + dismiss() + } + } + + final class EmojiPlusButton: UIView { + private let action: Action? + private let dismiss: () -> Void + + // MARK: - Settings + + public static let size: CGFloat = 28 + private let iconSize: CGFloat = 14 + + // MARK: - Lifecycle + + init(action: Action?, dismiss: @escaping () -> Void) { + self.action = action + self.dismiss = dismiss + + super.init(frame: CGRect.zero) + + setUpViewHierarchy() + } + + override init(frame: CGRect) { + preconditionFailure("Use init(for:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(for:) instead.") + } + + private func setUpViewHierarchy() { + // Icon image + let iconImageView = UIImageView(image: #imageLiteral(resourceName: "ic_plus_24").withRenderingMode(.alwaysTemplate)) + iconImageView.tintColor = Colors.text + iconImageView.set(.width, to: iconSize) + iconImageView.set(.height, to: iconSize) + iconImageView.contentMode = .scaleAspectFit + addSubview(iconImageView) + iconImageView.center(in: self) + + // Background + isUserInteractionEnabled = true + backgroundColor = Colors.sessionEmojiPlusButtonBackground + + // Tap gesture recognizer + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + addGestureRecognizer(tapGestureRecognizer) + } + + // MARK: - Interaction + + @objc private func handleTap() { + dismiss() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: { [weak self] in + self?.action?.work() + }) + } + } +} diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index d7d9ed68a..976e8febf 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -13,10 +13,34 @@ final class ContextMenuVC: UIViewController { private let cellViewModel: MessageViewModel private let actions: [Action] private let dismiss: () -> Void - + // MARK: - UI private lazy var blurView: UIVisualEffectView = UIVisualEffectView(effect: nil) + + private lazy var emojiBar: UIView = { + let result = UIView() + result.layer.shadowColor = UIColor.black.cgColor + result.layer.shadowOffset = CGSize.zero + result.layer.shadowOpacity = 0.4 + result.layer.shadowRadius = 4 + result.set(.height, to: ContextMenuVC.actionViewHeight) + + return result + }() + + private lazy var emojiPlusButton: EmojiPlusButton = { + let result = EmojiPlusButton( + action: self.actions.first(where: { $0.isEmojiPlus }), + dismiss: snDismiss + ) + result.set(.width, to: EmojiPlusButton.size) + result.set(.height, to: EmojiPlusButton.size) + result.layer.cornerRadius = EmojiPlusButton.size / 2 + result.layer.masksToBounds = true + + return result + }() private lazy var menuView: UIView = { let result: UIView = UIView() @@ -85,11 +109,6 @@ final class ContextMenuVC: UIViewController { snapshot.layer.shadowRadius = 4 view.addSubview(snapshot) - snapshot.pin(.left, to: .left, of: view, withInset: frame.origin.x) - snapshot.pin(.top, to: .top, of: view, withInset: frame.origin.y) - snapshot.set(.width, to: frame.width) - snapshot.set(.height, to: frame.height) - // Timestamp view.addSubview(timestampLabel) timestampLabel.center(.vertical, in: snapshot) @@ -101,6 +120,35 @@ final class ContextMenuVC: UIViewController { timestampLabel.pin(.left, to: .right, of: snapshot, withInset: Values.smallSpacing) } + // Emoji reacts + let emojiBarBackgroundView = UIView() + emojiBarBackgroundView.backgroundColor = Colors.receivedMessageBackground + emojiBarBackgroundView.layer.cornerRadius = ContextMenuVC.actionViewHeight / 2 + emojiBarBackgroundView.layer.masksToBounds = true + emojiBar.addSubview(emojiBarBackgroundView) + emojiBarBackgroundView.pin(to: emojiBar) + + emojiBar.addSubview(emojiPlusButton) + emojiPlusButton.pin(.right, to: .right, of: emojiBar, withInset: -Values.smallSpacing) + emojiPlusButton.center(.vertical, in: emojiBar) + + let emojiBarStackView = UIStackView( + arrangedSubviews: actions + .filter { $0.isEmojiAction } + .map { action -> EmojiReactsView in EmojiReactsView(for: action, dismiss: snDismiss) } + ) + emojiBarStackView.axis = .horizontal + emojiBarStackView.spacing = Values.smallSpacing + emojiBarStackView.layoutMargins = UIEdgeInsets(top: 0, left: Values.smallSpacing, bottom: 0, right: Values.smallSpacing) + emojiBarStackView.isLayoutMarginsRelativeArrangement = true + emojiBar.addSubview(emojiBarStackView) + emojiBarStackView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: emojiBar) + emojiBarStackView.pin(.right, to: .left, of: emojiPlusButton) + + // Hide the emoji bar if we have no emoji actions + emojiBar.isHidden = emojiBarStackView.arrangedSubviews.isEmpty + view.addSubview(emojiBar) + // Menu let menuBackgroundView = UIView() menuBackgroundView.backgroundColor = Colors.receivedMessageBackground @@ -111,7 +159,7 @@ final class ContextMenuVC: UIViewController { let menuStackView = UIStackView( arrangedSubviews: actions - .filter { !$0.isDismissAction } + .filter { !$0.isEmojiAction && !$0.isEmojiPlus && !$0.isDismissAction } .map { action -> ActionView in ActionView(for: action, dismiss: snDismiss) } ) menuStackView.axis = .vertical @@ -119,21 +167,27 @@ final class ContextMenuVC: UIViewController { menuStackView.pin(to: menuView) view.addSubview(menuView) - let menuHeight = (CGFloat(actions.count) * ContextMenuVC.actionViewHeight) - let spacing = Values.smallSpacing - // FIXME: Need to update this when an appropriate replacement is added (see https://teng.pub/technical/2021/11/9/uiapplication-key-window-replacement) - let margin = max(UIApplication.shared.keyWindow!.safeAreaInsets.bottom, Values.mediumSpacing) + // Constrains + let menuHeight: CGFloat = CGFloat(menuStackView.arrangedSubviews.count) * ContextMenuVC.actionViewHeight + let spacing: CGFloat = Values.smallSpacing + let targetFrame: CGRect = calculateFrame(menuHeight: menuHeight, spacing: spacing) + + snapshot.pin(.left, to: .left, of: view, withInset: targetFrame.origin.x) + snapshot.pin(.top, to: .top, of: view, withInset: targetFrame.origin.y) + snapshot.set(.width, to: targetFrame.width) + snapshot.set(.height, to: targetFrame.height) + emojiBar.pin(.bottom, to: .top, of: snapshot, withInset: -spacing) + menuView.pin(.top, to: .bottom, of: snapshot, withInset: spacing) - if frame.maxY + spacing + menuHeight > UIScreen.main.bounds.height - margin { - menuView.pin(.bottom, to: .top, of: snapshot, withInset: -spacing) - } - else { - menuView.pin(.top, to: .bottom, of: snapshot, withInset: spacing) - } - switch cellViewModel.variant { - case .standardOutgoing: menuView.pin(.right, to: .right, of: snapshot) - case .standardIncoming: menuView.pin(.left, to: .left, of: snapshot) + case .standardOutgoing: + menuView.pin(.right, to: .right, of: snapshot) + emojiBar.pin(.right, to: .right, of: snapshot) + + case .standardIncoming: + menuView.pin(.left, to: .left, of: snapshot) + emojiBar.pin(.left, to: .left, of: snapshot) + default: break // Should never occur } @@ -150,6 +204,40 @@ final class ContextMenuVC: UIViewController { self.menuView.alpha = 1 } } + + func calculateFrame(menuHeight: CGFloat, spacing: CGFloat) -> CGRect { + var finalFrame: CGRect = frame + let ratio: CGFloat = (frame.width / frame.height) + + // FIXME: Need to update this when an appropriate replacement is added (see https://teng.pub/technical/2021/11/9/uiapplication-key-window-replacement) + let topMargin = max(UIApplication.shared.keyWindow!.safeAreaInsets.top, Values.mediumSpacing) + let bottomMargin = max(UIApplication.shared.keyWindow!.safeAreaInsets.bottom, Values.mediumSpacing) + let diffY = finalFrame.height + menuHeight + Self.actionViewHeight + 2 * spacing + topMargin + bottomMargin - UIScreen.main.bounds.height + + if diffY > 0 { + // The screenshot needs to be shrinked. Menu + emoji bar + screenshot will fill the entire screen. + finalFrame.size.height -= diffY + let newWidth = ratio * finalFrame.size.height + if cellViewModel.variant == .standardOutgoing { + finalFrame.origin.x += finalFrame.size.width - newWidth + } + finalFrame.size.width = newWidth + finalFrame.origin.y = UIScreen.main.bounds.height - finalFrame.size.height - menuHeight - bottomMargin - spacing + } + else { + // The screenshot does NOT need to be shrinked. + if finalFrame.origin.y - Self.actionViewHeight - spacing < topMargin { + // Needs to move down + finalFrame.origin.y = topMargin + Self.actionViewHeight + spacing + } + if finalFrame.origin.y + finalFrame.size.height + spacing + menuHeight + bottomMargin > UIScreen.main.bounds.height { + // Needs to move up + finalFrame.origin.y = UIScreen.main.bounds.height - bottomMargin - menuHeight - spacing - finalFrame.size.height + } + } + + return finalFrame + } // MARK: - Layout @@ -160,6 +248,10 @@ final class ContextMenuVC: UIViewController { roundedRect: menuView.bounds, cornerRadius: ContextMenuVC.menuCornerRadius ).cgPath + emojiBar.layer.shadowPath = UIBezierPath( + roundedRect: emojiBar.bounds, + cornerRadius: (ContextMenuVC.actionViewHeight / 2) + ).cgPath } // MARK: - Interaction @@ -174,6 +266,7 @@ final class ContextMenuVC: UIViewController { animations: { [weak self] in self?.blurView.effect = nil self?.menuView.alpha = 0 + self?.emojiBar.alpha = 0 self?.snapshot.alpha = 0 self?.timestampLabel.alpha = 0 }, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index fab17974c..d2c150b9a 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -73,8 +73,7 @@ extension ConversationVC: let callVC = CallVC(for: call) callVC.conversationVC = self - self.inputAccessoryView?.isHidden = true - self.inputAccessoryView?.alpha = 0 + hideInputAccessoryView() present(callVC, animated: true, completion: nil) } @@ -85,7 +84,7 @@ extension ConversationVC: self.showBlockedModalIfNeeded() } - func showBlockedModalIfNeeded() -> Bool { + @discardableResult func showBlockedModalIfNeeded() -> Bool { guard self.viewModel.threadData.threadIsBlocked == true else { return false } let blockedModal = BlockedModal(publicKey: viewModel.threadData.threadId) @@ -642,7 +641,12 @@ extension ConversationVC: return result } - + + func hideInputAccessoryView() { + self.inputAccessoryView?.isHidden = true + self.inputAccessoryView?.alpha = 0 + } + func showInputAccessoryView() { UIView.animate(withDuration: 0.25, animations: { self.inputAccessoryView?.isHidden = false @@ -667,11 +671,13 @@ extension ConversationVC: contextMenuWindow == nil, let actions: [ContextMenuVC.Action] = ContextMenuVC.actions( for: cellViewModel, + recentEmojis: (self.viewModel.threadData.recentReactionEmoji ?? []).compactMap { EmojiWithSkinTones(rawValue: $0) }, currentUserIsOpenGroupModerator: OpenGroupManager.isUserModeratorOrAdmin( self.viewModel.threadData.currentUserPublicKey, for: self.viewModel.threadData.openGroupRoomToken, on: self.viewModel.threadData.openGroupServer ), + currentThreadIsMessageRequest: (self.viewModel.threadData.threadIsMessageRequest == true), delegate: self ) else { return } @@ -697,6 +703,7 @@ extension ConversationVC: self.contextMenuWindow?.backgroundColor = .clear self.contextMenuWindow?.rootViewController = self.contextMenuVC + self.contextMenuWindow?.overrideUserInterfaceStyle = (isDarkMode ? .dark : .light) self.contextMenuWindow?.makeKeyAndVisible() } @@ -948,10 +955,346 @@ extension ConversationVC: guard let threadId: String = targetThreadId else { return } let conversationVC: ConversationVC = ConversationVC(threadId: threadId, threadVariant: .contact) - self.navigationController?.pushViewController(conversationVC, animated: true) } + func showReactionList(_ cellViewModel: MessageViewModel, selectedReaction: EmojiWithSkinTones?) { + guard + cellViewModel.reactionInfo?.isEmpty == false && + ( + self.viewModel.threadData.threadVariant == .closedGroup || + self.viewModel.threadData.threadVariant == .openGroup + ), + let allMessages: [MessageViewModel] = self.viewModel.interactionData + .first(where: { $0.model == .messages })? + .elements + else { return } + + let reactionListSheet: ReactionListSheet = ReactionListSheet(for: cellViewModel.id) { [weak self] in + self?.currentReactionListSheet = nil + } + reactionListSheet.delegate = self + reactionListSheet.handleInteractionUpdates( + allMessages, + selectedReaction: selectedReaction, + initialLoad: true, + shouldShowClearAllButton: OpenGroupManager.isUserModeratorOrAdmin( + self.viewModel.threadData.currentUserPublicKey, + for: self.viewModel.threadData.openGroupRoomToken, + on: self.viewModel.threadData.openGroupServer + ) + ) + reactionListSheet.modalPresentationStyle = .overFullScreen + present(reactionListSheet, animated: true, completion: nil) + + // Store so we can updated the content based on the current VC + self.currentReactionListSheet = reactionListSheet + } + + func needsLayout(for cellViewModel: MessageViewModel, expandingReactions: Bool) { + guard + let messageSectionIndex: Int = self.viewModel.interactionData + .firstIndex(where: { $0.model == .messages }), + let targetMessageIndex = self.viewModel.interactionData[messageSectionIndex] + .elements + .firstIndex(where: { $0.id == cellViewModel.id }) + else { return } + + if expandingReactions { + self.viewModel.expandReactions(for: cellViewModel.id) + } + else { + self.viewModel.collapseReactions(for: cellViewModel.id) + } + + UIView.setAnimationsEnabled(false) + tableView.reloadRows( + at: [IndexPath(row: targetMessageIndex, section: messageSectionIndex)], + with: .none + ) + UIView.setAnimationsEnabled(true) + } + + func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones) { + react(cellViewModel, with: emoji.rawValue, remove: false) + } + + func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones) { + react(cellViewModel, with: emoji.rawValue, remove: true) + } + + func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String) { + guard cellViewModel.threadVariant == .openGroup else { return } + + Storage.shared + .read { db -> Promise in + guard + let openGroup: OpenGroup = try? OpenGroup + .fetchOne(db, id: cellViewModel.threadId), + let openGroupServerMessageId: Int64 = try? Interaction + .select(.openGroupServerMessageId) + .filter(id: cellViewModel.id) + .asRequest(of: Int64.self) + .fetchOne(db) + else { + return Promise(error: StorageError.objectNotFound) + } + + let pendingChange = OpenGroupManager + .addPendingReaction( + emoji: emoji, + id: openGroupServerMessageId, + in: openGroup.roomToken, + on: openGroup.server, + type: .removeAll + ) + + return OpenGroupAPI + .reactionDeleteAll( + db, + emoji: emoji, + id: openGroupServerMessageId, + in: openGroup.roomToken, + on: openGroup.server + ) + .map { _, response in + OpenGroupManager + .updatePendingChange( + pendingChange, + seqNo: response.seqNo + ) + } + } + .done { _ in + Storage.shared.writeAsync { db in + _ = try Reaction + .filter(Reaction.Columns.interactionId == cellViewModel.id) + .filter(Reaction.Columns.emoji == emoji) + .deleteAll(db) + } + } + .retainUntilComplete() + } + + func react(_ cellViewModel: MessageViewModel, with emoji: String, remove: Bool) { + guard cellViewModel.variant == .standardIncoming || cellViewModel.variant == .standardOutgoing else { + return + } + + let threadIsMessageRequest: Bool = (self.viewModel.threadData.threadIsMessageRequest == true) + guard !threadIsMessageRequest else { return } + + // Perform local rate limiting (don't allow more than 20 reactions within 60 seconds) + let sentTimestamp: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000)) + let recentReactionTimestamps: [Int64] = General.cache.wrappedValue.recentReactionTimestamps + + guard + recentReactionTimestamps.count < 20 || + (sentTimestamp - (recentReactionTimestamps.first ?? sentTimestamp)) > (60 * 1000) + else { return } + + General.cache.mutate { + $0.recentReactionTimestamps = Array($0.recentReactionTimestamps + .suffix(19)) + .appending(sentTimestamp) + } + + // Perform the sending logic + Storage.shared.writeAsync( + updates: { db in + guard let thread: SessionThread = try SessionThread.fetchOne(db, id: cellViewModel.threadId) else { + return + } + + // Update the thread to be visible + _ = try SessionThread + .filter(id: thread.id) + .updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true)) + + let pendingReaction: Reaction? = { + if remove { + return try? Reaction + .filter(Reaction.Columns.interactionId == cellViewModel.id) + .filter(Reaction.Columns.authorId == cellViewModel.currentUserPublicKey) + .filter(Reaction.Columns.emoji == emoji) + .fetchOne(db) + } else { + let sortId = Reaction.getSortId( + db, + interactionId: cellViewModel.id, + emoji: emoji + ) + + return Reaction( + interactionId: cellViewModel.id, + serverHash: nil, + timestampMs: sentTimestamp, + authorId: cellViewModel.currentUserPublicKey, + emoji: emoji, + count: 1, + sortId: sortId + ) + } + }() + + // Update the database + if remove { + try Reaction + .filter(Reaction.Columns.interactionId == cellViewModel.id) + .filter(Reaction.Columns.authorId == cellViewModel.currentUserPublicKey) + .filter(Reaction.Columns.emoji == emoji) + .deleteAll(db) + } + else { + try pendingReaction?.insert(db) + + // Add it to the recent list + Emoji.addRecent(db, emoji: emoji) + } + + if let openGroup: OpenGroup = try? OpenGroup.fetchOne(db, id: cellViewModel.threadId), + OpenGroupManager.isOpenGroupSupport(.reactions, on: openGroup.server) + { + // Send reaction to open groups + guard + let openGroupServerMessageId: Int64 = try? Interaction + .select(.openGroupServerMessageId) + .filter(id: cellViewModel.id) + .asRequest(of: Int64.self) + .fetchOne(db) + else { return } + + if remove { + let pendingChange = OpenGroupManager + .addPendingReaction( + emoji: emoji, + id: openGroupServerMessageId, + in: openGroup.roomToken, + on: openGroup.server, + type: .remove + ) + OpenGroupAPI + .reactionDelete( + db, + emoji: emoji, + id: openGroupServerMessageId, + in: openGroup.roomToken, + on: openGroup.server + ) + .map { _, response in + OpenGroupManager + .updatePendingChange( + pendingChange, + seqNo: response.seqNo + ) + } + .catch { [weak self] _ in + OpenGroupManager.removePendingChange(pendingChange) + + self?.handleReactionSentFailure( + pendingReaction, + remove: remove + ) + + } + .retainUntilComplete() + } else { + let pendingChange = OpenGroupManager + .addPendingReaction( + emoji: emoji, + id: openGroupServerMessageId, + in: openGroup.roomToken, + on: openGroup.server, + type: .add + ) + OpenGroupAPI + .reactionAdd( + db, + emoji: emoji, + id: openGroupServerMessageId, + in: openGroup.roomToken, + on: openGroup.server + ) + .map { _, response in + OpenGroupManager + .updatePendingChange( + pendingChange, + seqNo: response.seqNo + ) + } + .catch { [weak self] _ in + OpenGroupManager.removePendingChange(pendingChange) + + self?.handleReactionSentFailure( + pendingReaction, + remove: remove + ) + } + .retainUntilComplete() + } + + } else { + // Send the actual message + try MessageSender.send( + db, + message: VisibleMessage( + sentTimestamp: UInt64(sentTimestamp), + text: nil, + reaction: VisibleMessage.VMReaction( + timestamp: UInt64(cellViewModel.timestampMs), + publicKey: { + guard cellViewModel.variant == .standardIncoming else { + return cellViewModel.currentUserPublicKey + } + + return cellViewModel.authorId + }(), + emoji: emoji, + kind: (remove ? .remove : .react) + ) + ), + interactionId: cellViewModel.id, + in: thread + ) + } + } + ) + } + + func handleReactionSentFailure(_ pendingReaction: Reaction?, remove: Bool) { + guard let pendingReaction = pendingReaction else { return } + Storage.shared.writeAsync { db in + // Reverse the database + if remove { + try pendingReaction.insert(db) + } + else { + try Reaction + .filter(Reaction.Columns.interactionId == pendingReaction.interactionId) + .filter(Reaction.Columns.authorId == pendingReaction.authorId) + .filter(Reaction.Columns.emoji == pendingReaction.emoji) + .deleteAll(db) + } + } + } + + func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel) { + hideInputAccessoryView() + + let emojiPicker = EmojiPickerSheet( + completionHandler: { [weak self] emoji in + guard let emoji: EmojiWithSkinTones = emoji else { return } + + self?.react(cellViewModel, with: emoji) + }, + dismissHandler: { [weak self] in + self?.showInputAccessoryView() + } + ) + emojiPicker.modalPresentationStyle = .overFullScreen + present(emojiPicker, animated: true, completion: nil) + } + func contextMenuDismissed() { recoverInputView() } @@ -1132,7 +1475,60 @@ extension ConversationVC: on: openGroup.server ) ) - else { return } + else { + // If the message hasn't been sent yet then just delete locally + guard cellViewModel.state == .sending || cellViewModel.state == .failed else { + return + } + + // Retrieve any message send jobs for this interaction + let jobs: [Job] = Storage.shared + .read { db in + try? Job + .filter(Job.Columns.variant == Job.Variant.messageSend) + .filter(Job.Columns.interactionId == cellViewModel.id) + .fetchAll(db) + } + .defaulting(to: []) + + // If the job is currently running then wait until it's done before triggering + // the deletion + let targetJob: Job? = jobs.first(where: { JobRunner.isCurrentlyRunning($0) }) + + guard targetJob == nil else { + JobRunner.afterCurrentlyRunningJob(targetJob) { [weak self] result in + switch result { + // If it succeeded then we'll need to delete from the server so re-run + // this function (if we still don't have the server id for some reason + // then this would result in a local-only deletion which should be fine + case .succeeded: self?.delete(cellViewModel) + + // Otherwise we just need to cancel the pending job (in case it retries) + // and delete the interaction + default: + JobRunner.removePendingJob(targetJob) + + Storage.shared.writeAsync { db in + _ = try Interaction + .filter(id: cellViewModel.id) + .deleteAll(db) + } + } + } + return + } + + // If it's not currently running then remove any pending jobs (just to be safe) and + // delete the interaction locally + jobs.forEach { JobRunner.removePendingJob($0) } + + Storage.shared.writeAsync { db in + _ = try Interaction + .filter(id: cellViewModel.id) + .deleteAll(db) + } + return + } // Delete the message from the open group deleteRemotely( diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index b06c8d0b2..65e9f9a59 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -53,6 +53,10 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers var didFinishInitialLayout = false var scrollDistanceToBottomBeforeUpdate: CGFloat? var baselineKeyboardHeight: CGFloat = 0 + + // Reaction + var currentReactionListSheet: ReactionListSheet? + var reactionExpandedMessageIds: Set = [] /// This flag is used to temporarily prevent the ConversationVC from becoming the first responder (primarily used with /// custom transitions from preventing them from being buggy @@ -642,6 +646,11 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers return } + // Update the ReactionListSheet (if one exists) + if let messageUpdates: [MessageViewModel] = updatedData.first(where: { $0.model == .messages })?.elements { + self.currentReactionListSheet?.handleInteractionUpdates(messageUpdates) + } + // Store the 'sentMessageBeforeUpdate' state locally let didSendMessageBeforeUpdate: Bool = self.viewModel.sentMessageBeforeUpdate self.viewModel.sentMessageBeforeUpdate = false @@ -688,6 +697,17 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers let wasLoadingMore: Bool = self.isLoadingMore let wasOffsetCloseToBottom: Bool = self.isCloseToBottom let numItemsInUpdatedData: [Int] = updatedData.map { $0.elements.count } + let didSwapAllContent: Bool = (updatedData + .first(where: { $0.model == .messages })? + .elements + .contains(where: { + $0.id == self.viewModel.interactionData + .first(where: { $0.model == .messages })? + .elements + .first? + .id + })) + .defaulting(to: false) let itemChangeInfo: ItemChangeInfo? = { guard isInsert, @@ -720,7 +740,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers ) }() - guard !isInsert || wasLoadingMore || itemChangeInfo?.isInsertAtTop == true else { + guard !isInsert || itemChangeInfo?.isInsertAtTop == true else { self.viewModel.updateInteractionData(updatedData) self.tableView.reloadData() @@ -729,16 +749,27 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers if let focusedInteractionId: Int64 = self.focusedInteractionId { // If we had a focusedInteractionId then scroll to it (and hide the search // result bar loading indicator) - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in + let delay: DispatchTime = (didSwapAllContent ? + .now() : + (.now() + .milliseconds(100)) + ) + + DispatchQueue.main.asyncAfter(deadline: delay) { [weak self] in self?.searchController.resultsBar.stopLoading() self?.scrollToInteractionIfNeeded( with: focusedInteractionId, isAnimated: true, highlight: (self?.shouldHighlightNextScrollToInteraction == true) ) + + if wasLoadingMore { + // Complete page loading + self?.isLoadingMore = false + self?.autoLoadNextPageIfNeeded() + } } } - else if wasOffsetCloseToBottom { + else if wasOffsetCloseToBottom && !wasLoadingMore { // Scroll to the bottom if an interaction was just inserted and we either // just sent a message or are close enough to the bottom (wait a tiny fraction // to avoid buggy animation behaviour) @@ -746,6 +777,11 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers self?.scrollToBottom(isAnimated: true) } } + else if wasLoadingMore { + // Complete page loading + self.isLoadingMore = false + self.autoLoadNextPageIfNeeded() + } return } @@ -755,7 +791,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers /// /// Unfortunately the UITableView also does some weird things when updating (where it won't have updated it's internal data until /// after it performs the next layout); the below code checks a condition on layout and if it passes it calls a closure - if let itemChangeInfo: ItemChangeInfo = itemChangeInfo, (itemChangeInfo.isInsertAtTop || wasLoadingMore) { + if let itemChangeInfo: ItemChangeInfo = itemChangeInfo, itemChangeInfo.isInsertAtTop { let oldCellHeight: CGFloat = self.tableView.rectForRow(at: itemChangeInfo.oldVisibleIndexPath).height // The the user triggered the 'scrollToTop' animation (by tapping in the nav bar) then we @@ -789,7 +825,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers .rectForRow(at: itemChangeInfo.visibleIndexPath) .height let heightDiff: CGFloat = (oldCellHeight - (newTargetHeight ?? oldCellHeight)) - + self?.tableView.contentOffset.y += (calculatedRowHeights - heightDiff) } @@ -805,14 +841,38 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers ) } } - + // Complete page loading self?.isLoadingMore = false self?.autoLoadNextPageIfNeeded() } ) } + else if wasLoadingMore { + if let focusedInteractionId: Int64 = self.focusedInteractionId { + DispatchQueue.main.async { [weak self] in + // If we had a focusedInteractionId then scroll to it (and hide the search + // result bar loading indicator) + self?.searchController.resultsBar.stopLoading() + self?.scrollToInteractionIfNeeded( + with: focusedInteractionId, + isAnimated: true, + highlight: (self?.shouldHighlightNextScrollToInteraction == true) + ) + + // Complete page loading + self?.isLoadingMore = false + self?.autoLoadNextPageIfNeeded() + } + } + else { + // Complete page loading + self.isLoadingMore = false + self.autoLoadNextPageIfNeeded() + } + } + // Update the messages self.tableView.reload( using: changeset, deleteSectionsAnimation: .none, @@ -827,6 +887,8 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers } } + // MARK: Updating + private func performInitialScrollIfNeeded() { guard !hasPerformedInitialScroll && hasLoadedInitialThreadData && hasLoadedInitialInteractionData else { return @@ -837,13 +899,13 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers // the screen will scroll to the bottom instead of the first unread message if let focusedInteractionId: Int64 = self.viewModel.focusedInteractionId { self.scrollToInteractionIfNeeded(with: focusedInteractionId, isAnimated: false, highlight: true) - self.unreadCountView.alpha = self.scrollButton.alpha } else { self.scrollToBottom(isAnimated: false) } self.scrollButton.alpha = self.getScrollButtonOpacity() + self.unreadCountView.alpha = self.scrollButton.alpha self.hasPerformedInitialScroll = true // Now that the data has loaded we need to check if either of the "load more" sections are @@ -1018,6 +1080,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers let scrollButtonOpacity: CGFloat = (self?.getScrollButtonOpacity() ?? 0) self?.scrollButton.alpha = scrollButtonOpacity + self?.unreadCountView.alpha = scrollButtonOpacity self?.view.setNeedsLayout() self?.view.layoutIfNeeded() @@ -1132,6 +1195,8 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers cell.dynamicUpdate(with: cellViewModel, playbackInfo: updatedInfo) } }, + showExpandedReactions: viewModel.reactionExpandedInteractionIds + .contains(cellViewModel.id), lastSearchText: viewModel.lastSearchedText ) cell.delegate = self @@ -1225,6 +1290,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers self.scrollToInteractionIfNeeded( with: lastInteractionId, position: .bottom, + isJumpingToLastInteraction: true, isAnimated: true ) return @@ -1283,7 +1349,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers let contentOffsetY = tableView.contentOffset.y let x = (lastPageTop - ConversationVC.bottomInset - contentOffsetY).clamp(0, .greatestFiniteMagnitude) let a = 1 / (ConversationVC.scrollButtonFullVisibilityThreshold - ConversationVC.scrollButtonNoVisibilityThreshold) - return a * x + return max(0, min(1, a * x)) } // MARK: - Search @@ -1394,6 +1460,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers func scrollToInteractionIfNeeded( with interactionId: Int64, position: UITableView.ScrollPosition = .middle, + isJumpingToLastInteraction: Bool = false, isAnimated: Bool = true, highlight: Bool = false ) { @@ -1417,10 +1484,18 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers self.searchController.resultsBar.startLoading() DispatchQueue.global(qos: .default).async { [weak self] in - self?.viewModel.pagedDataObserver?.load(.untilInclusive( - id: interactionId, - padding: 5 - )) + if isJumpingToLastInteraction { + self?.viewModel.pagedDataObserver?.load(.jumpTo( + id: interactionId, + paddingForInclusive: 5 + )) + } + else { + self?.viewModel.pagedDataObserver?.load(.untilInclusive( + id: interactionId, + padding: 5 + )) + } } return } diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index c151ea467..757c53e26 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -132,10 +132,13 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { return ValueObservation .trackingConstantRegion { db -> SessionThreadViewModel? in let userPublicKey: String = getUserHexEncodedPublicKey(db) - - return try SessionThreadViewModel + let recentReactionEmoji: [String] = try Emoji.getRecent(db, withDefaultEmoji: true) + let threadViewModel: SessionThreadViewModel? = try SessionThreadViewModel .conversationQuery(threadId: threadId, userPublicKey: userPublicKey) .fetchOne(db) + + return threadViewModel + .map { $0.with(recentReactionEmoji: recentReactionEmoji) } } .removeDuplicates() } @@ -148,6 +151,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { public private(set) var unobservedInteractionDataChanges: [SectionModel]? public private(set) var interactionData: [SectionModel] = [] + public private(set) var reactionExpandedInteractionIds: Set = [] public private(set) var pagedDataObserver: PagedDatabaseObserver? public var onInteractionChange: (([SectionModel]) -> ())? { @@ -215,6 +219,18 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { joinToPagedType: MessageViewModel.AttachmentInteractionInfo.joinToViewModelQuerySQL, associateData: MessageViewModel.AttachmentInteractionInfo.createAssociateDataClosure() ), + AssociatedRecord( + trackedAgainst: Reaction.self, + observedChanges: [ + PagedData.ObservedChanges( + table: Reaction.self, + columns: [.count] + ) + ], + dataQuery: MessageViewModel.ReactionInfo.baseQuery, + joinToPagedType: MessageViewModel.ReactionInfo.joinToViewModelQuerySQL, + associateData: MessageViewModel.ReactionInfo.createAssociateDataClosure() + ), AssociatedRecord( trackedAgainst: ThreadTypingIndicator.self, observedChanges: [ @@ -294,6 +310,14 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { self.interactionData = updatedData } + public func expandReactions(for interactionId: Int64) { + reactionExpandedInteractionIds.insert(interactionId) + } + + public func collapseReactions(for interactionId: Int64) { + reactionExpandedInteractionIds.remove(interactionId) + } + // MARK: - Mentions public struct MentionInfo: FetchableRecord, Decodable { diff --git a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift new file mode 100644 index 000000000..a9bfcd1ae --- /dev/null +++ b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift @@ -0,0 +1,360 @@ +// Copyright Š 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUtilitiesKit + +protocol EmojiPickerCollectionViewDelegate: AnyObject { + func emojiPicker(_ emojiPicker: EmojiPickerCollectionView?, didSelectEmoji emoji: EmojiWithSkinTones) + func emojiPickerWillBeginDragging(_ emojiPicker: EmojiPickerCollectionView) +} + +class EmojiPickerCollectionView: UICollectionView { + let layout: UICollectionViewFlowLayout + + weak var pickerDelegate: EmojiPickerCollectionViewDelegate? + + private var recentEmoji: [EmojiWithSkinTones] = [] + var hasRecentEmoji: Bool { !recentEmoji.isEmpty } + + private var allSendableEmojiByCategory: [Emoji.Category: [EmojiWithSkinTones]] = [:] + private lazy var allSendableEmoji: [EmojiWithSkinTones] = { + return Array(allSendableEmojiByCategory.values).flatMap({$0}) + }() + + static let emojiWidth: CGFloat = 38 + static let margins: CGFloat = 16 + static let minimumSpacing: CGFloat = 10 + + public var searchText: String? { + didSet { + searchWithText(searchText) + } + } + + private var emojiSearchResults: [EmojiWithSkinTones] = [] + + public var isSearching: Bool { + if let searchText = searchText, searchText.count != 0 { + return true + } + + return false + } + + lazy var tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissSkinTonePicker)) + + // MARK: - Initialization + + init() { + layout = UICollectionViewFlowLayout() + layout.itemSize = CGSize(width: Self.emojiWidth, height: Self.emojiWidth) + layout.minimumInteritemSpacing = EmojiPickerCollectionView.minimumSpacing + layout.sectionInset = UIEdgeInsets(top: 0, leading: EmojiPickerCollectionView.margins, bottom: 0, trailing: EmojiPickerCollectionView.margins) + + super.init(frame: .zero, collectionViewLayout: layout) + + delegate = self + dataSource = self + + register(EmojiCell.self, forCellWithReuseIdentifier: EmojiCell.reuseIdentifier) + register( + EmojiSectionHeader.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: EmojiSectionHeader.reuseIdentifier + ) + + backgroundColor = .clear + + let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) + panGestureRecognizer.require(toFail: longPressGesture) + addGestureRecognizer(longPressGesture) + + addGestureRecognizer(tapGestureRecognizer) + tapGestureRecognizer.delegate = self + + // Fetch the emoji data from the database + let maybeEmojiData: (recent: [EmojiWithSkinTones], allGrouped: [Emoji.Category: [EmojiWithSkinTones]])? = Storage.shared.read { db in + // Some emoji have two different code points but identical appearances. Let's remove them! + // If we normalize to a different emoji than the one currently in our array, we want to drop + // the non-normalized variant if the normalized variant already exists. Otherwise, map to the + // normalized variant. + let recentEmoji: [EmojiWithSkinTones] = try Emoji.getRecent(db, withDefaultEmoji: false) + .compactMap { EmojiWithSkinTones(rawValue: $0) } + .reduce(into: [EmojiWithSkinTones]()) { result, emoji in + guard !emoji.isNormalized else { + result.append(emoji) + return + } + guard !result.contains(emoji.normalized) else { return } + + result.append(emoji.normalized) + } + let allSendableEmojiByCategory: [Emoji.Category: [EmojiWithSkinTones]] = Emoji.allSendableEmojiByCategoryWithPreferredSkinTones(db) + + return (recentEmoji, allSendableEmojiByCategory) + } + + if let emojiData: (recent: [EmojiWithSkinTones], allGrouped: [Emoji.Category: [EmojiWithSkinTones]]) = maybeEmojiData { + self.recentEmoji = emojiData.recent + self.allSendableEmojiByCategory = emojiData.allGrouped + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // This is not an exact calculation, but is simple and works for our purposes. + var numberOfColumns: Int { + Int((self.width()) / (EmojiPickerCollectionView.emojiWidth + EmojiPickerCollectionView.minimumSpacing)) + } + + // At max, we show 3 rows of recent emoji + private var maxRecentEmoji: Int { numberOfColumns * 3 } + private var categoryIndexOffset: Int { hasRecentEmoji ? 1 : 0} + + func emojiForSection(_ section: Int) -> [EmojiWithSkinTones] { + guard section > 0 || !hasRecentEmoji else { return Array(recentEmoji[0.. EmojiWithSkinTones? { + return isSearching ? emojiSearchResults[safe: indexPath.row] : emojiForSection(indexPath.section)[safe: indexPath.row] + } + + func nameForSection(_ section: Int) -> String? { + guard section > 0 || !hasRecentEmoji else { + return NSLocalizedString("EMOJI_CATEGORY_RECENTS_NAME", + comment: "The name for the emoji category 'Recents'") + } + + guard let category = Emoji.Category.allCases[safe: section - categoryIndexOffset] else { + owsFailDebug("Unexpectedly missing category for section \(section)") + return nil + } + + return category.localizedName + } + + // MARK: - Search + + func searchWithText(_ searchText: String?) { + if let searchText = searchText { + emojiSearchResults = allSendableEmoji.filter { emoji in + return emoji.baseEmoji?.name.range(of: searchText, options: [.caseInsensitive]) != nil + } + } else { + emojiSearchResults = [] + } + + reloadData() + } + + var scrollingToSection: Int? + func scrollToSectionHeader(_ section: Int, animated: Bool) { + guard let attributes = layoutAttributesForSupplementaryElement( + ofKind: UICollectionView.elementKindSectionHeader, + at: IndexPath(item: 0, section: section) + ) else { return } + scrollingToSection = section + setContentOffset(CGPoint(x: 0, y: (attributes.frame.minY - contentInset.top)), animated: animated) + } + + private weak var currentSkinTonePicker: EmojiSkinTonePicker? + + @objc + func handleLongPress(sender: UILongPressGestureRecognizer) { + + switch sender.state { + case .began: + let point = sender.location(in: self) + guard let indexPath = indexPathForItem(at: point) else { return } + guard let emoji = emojiForIndexPath(indexPath) else { return } + guard let cell = cellForItem(at: indexPath) else { return } + + currentSkinTonePicker?.dismiss() + currentSkinTonePicker = EmojiSkinTonePicker.present(referenceView: cell, emoji: emoji) { [weak self] emoji in + if let emoji: EmojiWithSkinTones = emoji { + Storage.shared.writeAsync { db in + emoji.baseEmoji?.setPreferredSkinTones( + db, + preferredSkinTonePermutation: emoji.skinTones + ) + } + + self?.pickerDelegate?.emojiPicker(self, didSelectEmoji: emoji) + } + + self?.currentSkinTonePicker?.dismiss() + self?.currentSkinTonePicker = nil + } + case .changed: + currentSkinTonePicker?.didChangeLongPress(sender) + case .ended: + currentSkinTonePicker?.didEndLongPress(sender) + default: + break + } + } + + @objc + func dismissSkinTonePicker() { + currentSkinTonePicker?.dismiss() + currentSkinTonePicker = nil + } +} + +extension EmojiPickerCollectionView: UIGestureRecognizerDelegate { + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer == tapGestureRecognizer { + return currentSkinTonePicker != nil + } + + return true + } +} + +extension EmojiPickerCollectionView: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let emoji = emojiForIndexPath(indexPath) else { + return owsFailDebug("Missing emoji for indexPath \(indexPath)") + } + + pickerDelegate?.emojiPicker(self, didSelectEmoji: emoji) + } +} + +extension EmojiPickerCollectionView: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return isSearching ? emojiSearchResults.count : emojiForSection(section).count + } + + func numberOfSections(in collectionView: UICollectionView) -> Int { + return isSearching ? 1 : Emoji.Category.allCases.count + categoryIndexOffset + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = dequeueReusableCell(withReuseIdentifier: EmojiCell.reuseIdentifier, for: indexPath) + + guard let emojiCell = cell as? EmojiCell else { + owsFailDebug("unexpected cell type") + return cell + } + + guard let emoji = emojiForIndexPath(indexPath) else { + owsFailDebug("unexpected indexPath") + return cell + } + + emojiCell.configure(emoji: emoji) + + return cell + } + + func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + + let supplementaryView = dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: EmojiSectionHeader.reuseIdentifier, + for: indexPath + ) + + guard let sectionHeader = supplementaryView as? EmojiSectionHeader else { + owsFailDebug("unexpected supplementary view type") + return supplementaryView + } + + sectionHeader.label.text = nameForSection(indexPath.section) + + return sectionHeader + } +} + +extension EmojiPickerCollectionView: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + referenceSizeForHeaderInSection section: Int) -> CGSize { + guard !isSearching else { + return CGSize.zero + } + + let measureCell = EmojiSectionHeader() + measureCell.label.text = nameForSection(section) + return measureCell.sizeThatFits(CGSize(width: self.width(), height: .greatestFiniteMagnitude)) + } +} + +private class EmojiCell: UICollectionViewCell { + static let reuseIdentifier = "EmojiCell" + + let emojiLabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .clear + + emojiLabel.font = .boldSystemFont(ofSize: 32) + contentView.addSubview(emojiLabel) + emojiLabel.autoPinEdgesToSuperviewEdges() + + // For whatever reason, some emoji glyphs occasionally have different typographic widths on certain devices + // e.g. 👩‍đŸĻ°: 36x38.19, 👱‍♀ī¸: 40x38. (See: commit message for more info) + // To workaround this, we can clip the label instead of truncating. It appears to only clip the additional + // typographic space. In either case, it's better than truncating and seeing an ellipsis. + emojiLabel.lineBreakMode = .byClipping + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(emoji: EmojiWithSkinTones) { + emojiLabel.text = emoji.rawValue + } +} + +private class EmojiSectionHeader: UICollectionReusableView { + static let reuseIdentifier = "EmojiSectionHeader" + + let label = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + + layoutMargins = UIEdgeInsets( + top: 16, + leading: EmojiPickerCollectionView.margins, + bottom: 6, + trailing: EmojiPickerCollectionView.margins + ) + + label.font = .systemFont(ofSize: Values.smallFontSize) + label.textColor = Colors.text + addSubview(label) + label.autoPinEdgesToSuperviewMargins() + label.setCompressionResistanceHigh() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + var labelSize = label.sizeThatFits(size) + labelSize.width += layoutMargins.left + layoutMargins.right + labelSize.height += layoutMargins.top + layoutMargins.bottom + return labelSize + } +} diff --git a/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift new file mode 100644 index 000000000..f2c935f1f --- /dev/null +++ b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift @@ -0,0 +1,138 @@ +// Copyright Š 2022 Rangeproof Pty Ltd. All rights reserved. + +class EmojiPickerSheet: BaseVC { + let completionHandler: (EmojiWithSkinTones?) -> Void + let dismissHandler: () -> Void + + // MARK: Components + + private lazy var contentView: UIView = { + let result = UIView() + let line = UIView() + line.set(.height, to: 0.5) + line.backgroundColor = Colors.border.withAlphaComponent(0.5) + result.addSubview(line) + line.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: result) + result.backgroundColor = Colors.modalBackground + return result + }() + + private let collectionView = EmojiPickerCollectionView() + + private lazy var searchBar: SearchBar = { + let result = SearchBar() + result.tintColor = Colors.text + result.backgroundColor = .clear + result.delegate = self + return result + }() + + // MARK: Lifecycle + + init(completionHandler: @escaping (EmojiWithSkinTones?) -> Void, dismissHandler: @escaping () -> Void) { + self.completionHandler = completionHandler + self.dismissHandler = dismissHandler + super.init(nibName: nil, bundle: nil) + } + + public required init() { + fatalError("init() has not been implemented") + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + setUpViewHierarchy() + } + + private func setUpViewHierarchy() { + view.addSubview(contentView) + contentView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.bottom ], to: view) + contentView.set(.height, to: 440) + populateContentView() + } + + private func populateContentView() { + let topStackView = UIStackView() + topStackView.axis = .horizontal + topStackView.isLayoutMarginsRelativeArrangement = true + topStackView.spacing = 8 + + topStackView.addArrangedSubview(searchBar) + + contentView.addSubview(topStackView) + + topStackView.autoPinWidthToSuperview() + topStackView.autoPinEdge(toSuperviewEdge: .top) + + contentView.addSubview(collectionView) + collectionView.autoPinEdge(.top, to: .bottom, of: searchBar) + collectionView.autoPinEdge(.bottom, to: .bottom, of: contentView) + collectionView.autoPinWidthToSuperview() + collectionView.pickerDelegate = self + collectionView.alwaysBounceVertical = true + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + coordinator.animate(alongsideTransition: { _ in + self.collectionView.reloadData() + }, completion: nil) + } + + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // Ensure the scrollView's layout has completed + // as we're about to use its bounds to calculate + // the masking view and contentOffset. + contentView.layoutIfNeeded() + } + + // MARK: Interaction + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + let touch = touches.first! + let location = touch.location(in: view) + if contentView.frame.contains(location) { + super.touchesBegan(touches, with: event) + } else { + close() + } + } + + @objc func close() { + dismiss(animated: true, completion: dismissHandler) + } +} + +extension EmojiPickerSheet: EmojiPickerCollectionViewDelegate { + func emojiPickerWillBeginDragging(_ emojiPicker: EmojiPickerCollectionView) { + searchBar.resignFirstResponder() + } + + func emojiPicker(_ emojiPicker: EmojiPickerCollectionView?, didSelectEmoji emoji: EmojiWithSkinTones) { + completionHandler(emoji) + dismiss(animated: true, completion: dismissHandler) + } +} + +extension EmojiPickerSheet: UISearchBarDelegate { + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + collectionView.searchText = searchText + } + + func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool { + searchBar.showsCancelButton = true + return true + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + searchBar.showsCancelButton = false + searchBar.resignFirstResponder() + } +} + diff --git a/Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift b/Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift new file mode 100644 index 000000000..37eb27e42 --- /dev/null +++ b/Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift @@ -0,0 +1,305 @@ + +import Foundation + +class EmojiSkinTonePicker: UIView { + let emoji: Emoji + let preferredSkinTonePermutation: [Emoji.SkinTone]? + let completion: (EmojiWithSkinTones?) -> Void + + private let referenceOverlay = UIView() + private let containerView = UIView() + + class func present( + referenceView: UIView, + emoji: EmojiWithSkinTones, + completion: @escaping (EmojiWithSkinTones?) -> Void + ) -> EmojiSkinTonePicker? { + guard let baseEmoji = emoji.baseEmoji, baseEmoji.hasSkinTones else { return nil } + + UIImpactFeedbackGenerator(style: .light).impactOccurred() + + let picker = EmojiSkinTonePicker(emoji: emoji, completion: completion) + + guard let superview = referenceView.superview else { + owsFailDebug("reference is missing superview") + return nil + } + + superview.addSubview(picker) + + picker.referenceOverlay.autoMatch(.width, to: .width, of: referenceView) + picker.referenceOverlay.autoMatch(.height, to: .height, of: referenceView, withOffset: 30) + picker.referenceOverlay.autoPinEdge(.leading, to: .leading, of: referenceView) + + let leadingConstraint = picker.autoPinEdge(toSuperviewEdge: .leading) + + picker.layoutIfNeeded() + + let halfWidth = picker.width() / 2 + let margin: CGFloat = 8 + + if (halfWidth + margin) > referenceView.center.x { + leadingConstraint.constant = margin + } else if (halfWidth + margin) > (superview.width() - referenceView.center.x) { + leadingConstraint.constant = superview.width() - picker.width() - margin + } else { + leadingConstraint.constant = referenceView.center.x - halfWidth + } + + let distanceFromTop = referenceView.frame.minY - superview.bounds.minY + if distanceFromTop > picker.containerView.height() { + picker.containerView.autoPinEdge(toSuperviewEdge: .top) + picker.referenceOverlay.autoPinEdge(.top, to: .bottom, of: picker.containerView, withOffset: -20) + picker.referenceOverlay.autoPinEdge(toSuperviewEdge: .bottom) + picker.autoPinEdge(.bottom, to: .bottom, of: referenceView) + } else { + picker.containerView.autoPinEdge(toSuperviewEdge: .bottom) + picker.referenceOverlay.autoPinEdge(.bottom, to: .top, of: picker.containerView, withOffset: 20) + picker.referenceOverlay.autoPinEdge(toSuperviewEdge: .top) + picker.autoPinEdge(.top, to: .top, of: referenceView) + } + + picker.alpha = 0 + UIView.animate(withDuration: 0.12) { picker.alpha = 1 } + + return picker + } + + func dismiss() { + UIView.animate(withDuration: 0.12, animations: { self.alpha = 0 }) { _ in + self.removeFromSuperview() + } + } + + func didChangeLongPress(_ sender: UILongPressGestureRecognizer) { + guard let singleSelectionButtons = singleSelectionButtons else { return } + + if referenceOverlay.frame.contains(sender.location(in: self)) { + singleSelectionButtons.forEach { $0.isSelected = false } + } else { + let point = sender.location(in: containerView) + let previouslySelectedButton = singleSelectionButtons.first { $0.isSelected } + singleSelectionButtons.forEach { $0.isSelected = $0.frame.insetBy(dx: -3, dy: -80).contains(point) } + let selectedButton = singleSelectionButtons.first { $0.isSelected } + + if let selectedButton = selectedButton, selectedButton != previouslySelectedButton { + SelectionHapticFeedback().selectionChanged() + } + } + } + + func didEndLongPress(_ sender: UILongPressGestureRecognizer) { + guard let singleSelectionButtons = singleSelectionButtons else { return } + + let point = sender.location(in: containerView) + if referenceOverlay.frame.contains(sender.location(in: self)) { + // Do nothing. + } else if let selectedButton = singleSelectionButtons.first(where: { + $0.frame.insetBy(dx: -3, dy: -80).contains(point) + }) { + selectedButton.sendActions(for: .touchUpInside) + } else { + dismiss() + } + } + + init(emoji: EmojiWithSkinTones, completion: @escaping (EmojiWithSkinTones?) -> Void) { + owsAssertDebug(emoji.baseEmoji!.hasSkinTones) + + self.emoji = emoji.baseEmoji! + self.preferredSkinTonePermutation = emoji.skinTones + self.completion = completion + + super.init(frame: .zero) + + layer.shadowOffset = .zero + layer.shadowOpacity = 0.25 + layer.shadowRadius = 4 + + referenceOverlay.backgroundColor = Colors.modalBackground + referenceOverlay.layer.cornerRadius = 9 + addSubview(referenceOverlay) + + containerView.layoutMargins = UIEdgeInsets(top: 9, leading: 16, bottom: 9, trailing: 16) + containerView.backgroundColor = Colors.modalBackground + containerView.layer.cornerRadius = 11 + addSubview(containerView) + containerView.autoPinWidthToSuperview() + containerView.setCompressionResistanceHigh() + + if emoji.baseEmoji!.allowsMultipleSkinTones { + prepareForMultipleSkinTones() + } else { + prepareForSingleSkinTone() + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Single Skin Tone + + private lazy var yellowEmoji = EmojiWithSkinTones(baseEmoji: emoji, skinTones: nil) + private lazy var yellowButton = button(for: yellowEmoji) { [weak self] emojiWithSkinTone in + self?.completion(emojiWithSkinTone) + } + + private var singleSelectionButtons: [UIButton]? + private func prepareForSingleSkinTone() { + let hStack = UIStackView() + hStack.axis = .horizontal + hStack.spacing = 8 + containerView.addSubview(hStack) + hStack.autoPinEdgesToSuperviewMargins() + + hStack.addArrangedSubview(yellowButton) + + hStack.addArrangedSubview(.spacer(withWidth: 2)) + + let divider = UIView() + divider.autoSetDimension(.width, toSize: 1) + divider.backgroundColor = isDarkMode ? .ows_gray75 : .ows_gray05 + hStack.addArrangedSubview(divider) + + hStack.addArrangedSubview(.spacer(withWidth: 2)) + + let skinToneButtons = self.skinToneButtons(for: emoji) { [weak self] emojiWithSkinTone in + self?.completion(emojiWithSkinTone) + } + + singleSelectionButtons = skinToneButtons.map { $0.button } + singleSelectionButtons?.forEach { hStack.addArrangedSubview($0) } + singleSelectionButtons?.append(yellowButton) + } + + // MARK: - Multiple Skin Tones + + private lazy var skinToneComponentEmoji: [Emoji] = { + guard let skinToneComponentEmoji = emoji.skinToneComponentEmoji else { + owsFailDebug("missing skin tone component emoji \(emoji)") + return [] + } + return skinToneComponentEmoji + }() + + private var buttonsPerComponentEmojiIndex = [Int: [(Emoji.SkinTone, UIButton)]]() + private lazy var skinToneButton = button(for: EmojiWithSkinTones( + baseEmoji: emoji, + skinTones: .init(repeating: .medium, count: skinToneComponentEmoji.count) + )) { [weak self] _ in + guard let self = self else { return } + guard self.selectedSkinTones.count == self.skinToneComponentEmoji.count else { return } + self.completion(EmojiWithSkinTones(baseEmoji: self.emoji, skinTones: self.selectedSkinTones)) + } + + private var selectedSkinTones = [Emoji.SkinTone]() { + didSet { + if selectedSkinTones.count == skinToneComponentEmoji.count { + skinToneButton.setTitle( + EmojiWithSkinTones( + baseEmoji: emoji, + skinTones: selectedSkinTones + ).rawValue, + for: .normal + ) + skinToneButton.isEnabled = true + skinToneButton.alpha = 1 + } else { + skinToneButton.setTitle( + EmojiWithSkinTones( + baseEmoji: emoji, + skinTones: [.medium] + ).rawValue, + for: .normal + ) + skinToneButton.isEnabled = false + skinToneButton.alpha = 0.2 + } + } + } + + private var skinTonePerComponentEmojiIndex = [Int: Emoji.SkinTone]() { + didSet { + var selectedSkinTones = [Emoji.SkinTone]() + for idx in skinToneComponentEmoji.indices { + for (skinTone, button) in buttonsPerComponentEmojiIndex[idx] ?? [] { + if skinTonePerComponentEmojiIndex[idx] == skinTone { + selectedSkinTones.append(skinTone) + button.isSelected = true + } else { + button.isSelected = false + } + } + } + self.selectedSkinTones = selectedSkinTones + } + } + + private func prepareForMultipleSkinTones() { + let vStack = UIStackView() + vStack.axis = .vertical + vStack.spacing = 6 + containerView.addSubview(vStack) + vStack.autoPinEdgesToSuperviewMargins() + + for (idx, emoji) in skinToneComponentEmoji.enumerated() { + let skinToneButtons = self.skinToneButtons(for: emoji) { [weak self] emojiWithSkinTone in + self?.skinTonePerComponentEmojiIndex[idx] = emojiWithSkinTone.skinTones?.first + } + buttonsPerComponentEmojiIndex[idx] = skinToneButtons + + let hStack = UIStackView(arrangedSubviews: skinToneButtons.map { $0.button }) + hStack.axis = .horizontal + hStack.spacing = 6 + vStack.addArrangedSubview(hStack) + + skinTonePerComponentEmojiIndex[idx] = preferredSkinTonePermutation?[safe: idx] + + // If there's only one preferred skin tone, all the component emoji use it. + if preferredSkinTonePermutation?.count == 1 { + skinTonePerComponentEmojiIndex[idx] = preferredSkinTonePermutation?.first + } else { + skinTonePerComponentEmojiIndex[idx] = preferredSkinTonePermutation?[safe: idx] + } + } + + let divider = UIView() + divider.autoSetDimension(.height, toSize: 1) + divider.backgroundColor = isDarkMode ? .ows_gray75 : .ows_gray05 + vStack.addArrangedSubview(divider) + + let leftSpacer = UIView.hStretchingSpacer() + let middleSpacer = UIView.hStretchingSpacer() + let rightSpacer = UIView.hStretchingSpacer() + + let hStack = UIStackView(arrangedSubviews: [leftSpacer, yellowButton, middleSpacer, skinToneButton, rightSpacer]) + hStack.axis = .horizontal + vStack.addArrangedSubview(hStack) + + leftSpacer.autoMatch(.width, to: .width, of: rightSpacer) + middleSpacer.autoMatch(.width, to: .width, of: rightSpacer) + } + + // MARK: - Button Helpers + + func skinToneButtons(for emoji: Emoji, handler: @escaping (EmojiWithSkinTones) -> Void) -> [(skinTone: Emoji.SkinTone, button: UIButton)] { + var buttons = [(Emoji.SkinTone, UIButton)]() + for skinTone in Emoji.SkinTone.allCases { + let emojiWithSkinTone = EmojiWithSkinTones(baseEmoji: emoji, skinTones: [skinTone]) + buttons.append((skinTone, button(for: emojiWithSkinTone, handler: handler))) + } + return buttons + } + + func button(for emoji: EmojiWithSkinTones, handler: @escaping (EmojiWithSkinTones) -> Void) -> UIButton { + let button = OWSButton { handler(emoji) } + button.titleLabel?.font = .boldSystemFont(ofSize: 32) + button.setTitle(emoji.rawValue, for: .normal) + button.setBackgroundImage(UIImage(color: isDarkMode ? .ows_gray60 : .ows_gray25), for: .selected) + button.layer.cornerRadius = 6 + button.clipsToBounds = true + button.autoSetDimensions(to: CGSize(width: 38, height: 38)) + return button + } +} diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index 474c4d6db..2bbc8d332 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -397,7 +397,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M inputTextView.becomeFirstResponder() } - func handleLongPress() { + func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { // Not relevant in this case } @@ -455,6 +455,10 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M func handleMentionSelected(_ mentionInfo: ConversationViewModel.MentionInfo, from view: MentionSelectionView) { delegate?.handleMentionSelected(mentionInfo, from: view) } + + func tapableLabel(_ label: TappableLabel, didTapUrl url: String, atRange range: NSRange) { + // Do nothing + } // MARK: - Convenience diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index ff98141ff..36e0ced6e 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -102,6 +102,7 @@ final class CallMessageCell: MessageCell { with cellViewModel: MessageViewModel, mediaCache: NSCache, playbackInfo: ConversationViewModel.PlaybackInfo?, + showExpandedReactions: Bool, lastSearchText: String? ) { guard diff --git a/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift b/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift index 16eb6e9b5..7ef5e2f63 100644 --- a/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift +++ b/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift @@ -48,7 +48,7 @@ final class LinkPreviewView: UIView { return result }() - private lazy var bodyTextViewContainer: UIView = UIView() + private lazy var bodyTappableLabelContainer: UIView = UIView() private lazy var hStackViewContainer: UIView = UIView() @@ -67,8 +67,8 @@ final class LinkPreviewView: UIView { return result }() - - var bodyTextView: UITextView? + + var bodyTappableLabel: TappableLabel? // MARK: - Initialization @@ -110,7 +110,7 @@ final class LinkPreviewView: UIView { hStackView.pin(to: hStackViewContainer) // Vertical stack view - let vStackView = UIStackView(arrangedSubviews: [ hStackViewContainer, bodyTextViewContainer ]) + let vStackView = UIStackView(arrangedSubviews: [ hStackViewContainer, bodyTappableLabelContainer ]) vStackView.axis = .vertical addSubview(vStackView) vStackView.pin(to: self) @@ -129,7 +129,7 @@ final class LinkPreviewView: UIView { public func update( with state: LinkPreviewState, isOutgoing: Bool, - delegate: (UITextViewDelegate & BodyTextViewDelegate)? = nil, + delegate: TappableLabelDelegate? = nil, cellViewModel: MessageViewModel? = nil, bodyLabelTextColor: UIColor? = nil, lastSearchText: String? = nil @@ -184,10 +184,10 @@ final class LinkPreviewView: UIView { } // Body text view - bodyTextViewContainer.subviews.forEach { $0.removeFromSuperview() } + bodyTappableLabelContainer.subviews.forEach { $0.removeFromSuperview() } if let cellViewModel: MessageViewModel = cellViewModel { - let bodyTextView = VisibleMessageCell.getBodyTextView( + let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel( for: cellViewModel, with: maxWidth, textColor: (bodyLabelTextColor ?? sentLinkPreviewTextColor), @@ -195,9 +195,9 @@ final class LinkPreviewView: UIView { delegate: delegate ) - self.bodyTextView = bodyTextView - bodyTextViewContainer.addSubview(bodyTextView) - bodyTextView.pin(to: bodyTextViewContainer, withInset: 12) + self.bodyTappableLabel = bodyTappableLabel + bodyTappableLabelContainer.addSubview(bodyTappableLabel) + bodyTappableLabel.pin(to: bodyTappableLabelContainer, withInset: 12) } if state is LinkPreview.DraftState { diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift new file mode 100644 index 000000000..982195f50 --- /dev/null +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -0,0 +1,171 @@ +// Copyright Š 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUIKit + +final class ReactionContainerView: UIView { + var showingAllReactions = false + private var showNumbers = true + private var maxEmojisPerLine = isIPhone6OrSmaller ? 5 : 6 + + var reactions: [ReactionViewModel] = [] + var reactionViews: [ReactionButton] = [] + + // MARK: - UI + + private lazy var mainStackView: UIStackView = { + let result = UIStackView(arrangedSubviews: [ reactionContainerView ]) + result.axis = .vertical + result.spacing = Values.smallSpacing + result.alignment = .center + return result + }() + + private lazy var reactionContainerView: UIStackView = { + let result = UIStackView() + result.axis = .vertical + result.spacing = Values.smallSpacing + result.alignment = .leading + return result + }() + + var expandButton: ExpandingReactionButton? + + var collapseButton: UIStackView = { + let arrow = UIImageView(image: UIImage(named: "ic_chevron_up")?.resizedImage(to: CGSize(width: 15, height: 13))?.withRenderingMode(.alwaysTemplate)) + arrow.tintColor = Colors.text + + let textLabel = UILabel() + textLabel.text = "Show less" + textLabel.font = .systemFont(ofSize: Values.verySmallFontSize) + textLabel.textColor = Colors.text + + let result = UIStackView(arrangedSubviews: [ UIView.hStretchingSpacer(), arrow, textLabel, UIView.hStretchingSpacer() ]) + result.spacing = Values.verySmallSpacing + result.alignment = .center + return result + }() + + // MARK: - Lifecycle + + init() { + super.init(frame: CGRect.zero) + setUpViewHierarchy() + } + + override init(frame: CGRect) { + preconditionFailure("Use init(viewItem:textColor:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(viewItem:textColor:) instead.") + } + + private func setUpViewHierarchy() { + addSubview(mainStackView) + + mainStackView.pin(to: self) + } + + public func update(_ reactions: [ReactionViewModel], showNumbers: Bool) { + self.reactions = reactions + self.showNumbers = showNumbers + + prepareForUpdate() + + if showingAllReactions { + updateAllReactions() + } + else { + updateCollapsedReactions(reactions) + } + } + + private func updateCollapsedReactions(_ reactions: [ReactionViewModel]) { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = Values.smallSpacing + stackView.alignment = .center + + var displayedReactions: [ReactionViewModel] + var expandButtonReactions: [EmojiWithSkinTones] + + if reactions.count > maxEmojisPerLine { + displayedReactions = Array(reactions[0...(maxEmojisPerLine - 3)]) + expandButtonReactions = Array(reactions[(maxEmojisPerLine - 2)...maxEmojisPerLine]) + .map { $0.emoji } + } + else { + displayedReactions = reactions + expandButtonReactions = [] + } + + for reaction in displayedReactions { + let reactionView = ReactionButton(viewModel: reaction, showNumber: showNumbers) + stackView.addArrangedSubview(reactionView) + reactionViews.append(reactionView) + } + + if expandButtonReactions.count > 0 { + let expandButton: ExpandingReactionButton = ExpandingReactionButton(emojis: expandButtonReactions) + stackView.addArrangedSubview(expandButton) + + self.expandButton = expandButton + } + else { + expandButton = nil + } + + reactionContainerView.addArrangedSubview(stackView) + } + + private func updateAllReactions() { + var reactions = self.reactions + var numberOfLines = 0 + + while reactions.count > 0 { + var line: [ReactionViewModel] = [] + + while reactions.count > 0 && line.count < maxEmojisPerLine { + line.append(reactions.removeFirst()) + } + + updateCollapsedReactions(line) + numberOfLines += 1 + } + + if numberOfLines > 1 { + mainStackView.addArrangedSubview(collapseButton) + } + else { + showingAllReactions = false + } + } + + private func prepareForUpdate() { + for subview in reactionContainerView.arrangedSubviews { + reactionContainerView.removeArrangedSubview(subview) + subview.removeFromSuperview() + } + + mainStackView.removeArrangedSubview(collapseButton) + collapseButton.removeFromSuperview() + reactionViews = [] + } + + public func showAllEmojis() { + guard !showingAllReactions else { return } + + showingAllReactions = true + update(reactions, showNumbers: showNumbers) + } + + public func showLessEmojis() { + guard showingAllReactions else { return } + + showingAllReactions = false + update(reactions, showNumbers: showNumbers) + } +} + + diff --git a/Session/Conversations/Message Cells/Content Views/ReactionView.swift b/Session/Conversations/Message Cells/Content Views/ReactionView.swift new file mode 100644 index 000000000..01cf006f3 --- /dev/null +++ b/Session/Conversations/Message Cells/Content Views/ReactionView.swift @@ -0,0 +1,127 @@ +// Copyright Š 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUIKit + +public struct ReactionViewModel: Hashable { + let emoji: EmojiWithSkinTones + let number: Int + let showBorder: Bool +} + +final class ReactionButton: UIView { + let viewModel: ReactionViewModel + let showNumber: Bool + + // MARK: - Settings + + private var height: CGFloat = 22 + private var fontSize: CGFloat = Values.verySmallFontSize + private var spacing: CGFloat = Values.verySmallSpacing + + // MARK: - Lifecycle + + init(viewModel: ReactionViewModel, showNumber: Bool = true) { + self.viewModel = viewModel + self.showNumber = showNumber + + super.init(frame: CGRect.zero) + + setUpViewHierarchy() + } + + override init(frame: CGRect) { + preconditionFailure("Use init(viewItem:textColor:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(viewItem:textColor:) instead.") + } + + private func setUpViewHierarchy() { + let emojiLabel = UILabel() + emojiLabel.text = viewModel.emoji.rawValue + emojiLabel.font = .systemFont(ofSize: fontSize) + + let stackView = UIStackView(arrangedSubviews: [ emojiLabel ]) + stackView.axis = .horizontal + stackView.spacing = spacing + stackView.alignment = .center + stackView.layoutMargins = UIEdgeInsets(top: 0, left: Values.smallSpacing, bottom: 0, right: Values.smallSpacing) + stackView.isLayoutMarginsRelativeArrangement = true + addSubview(stackView) + stackView.pin(to: self) + + set(.height, to: self.height) + backgroundColor = Colors.receivedMessageBackground + layer.cornerRadius = self.height / 2 + + if viewModel.showBorder { + self.addBorder(with: Colors.accent) + } + + if showNumber || viewModel.number > 1 { + let numberLabel = UILabel() + numberLabel.text = viewModel.number < 1000 ? "\(viewModel.number)" : String(format: "%.1f", Float(viewModel.number) / 1000) + "k" + numberLabel.font = .systemFont(ofSize: fontSize) + numberLabel.textColor = Colors.text + stackView.addArrangedSubview(numberLabel) + } + } +} + +final class ExpandingReactionButton: UIView { + private let emojis: [EmojiWithSkinTones] + + // MARK: - Settings + + private let size: CGFloat = 22 + private let margin: CGFloat = 15 + + // MARK: - Lifecycle + + init(emojis: [EmojiWithSkinTones]) { + self.emojis = emojis + + super.init(frame: CGRect.zero) + + setUpViewHierarchy() + } + + override init(frame: CGRect) { + preconditionFailure("Use init(viewItem:textColor:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(viewItem:textColor:) instead.") + } + + private func setUpViewHierarchy() { + var rightMargin: CGFloat = 0 + + for emoji in self.emojis.reversed() { + let container = UIView() + container.set(.width, to: size) + container.set(.height, to: size) + container.backgroundColor = Colors.receivedMessageBackground + container.layer.cornerRadius = size / 2 + container.layer.borderWidth = 1 + // FIXME: This is going to have issues when swapping between light/dark mode + container.layer.borderColor = (isDarkMode ? UIColor.black.cgColor : UIColor.white.cgColor) + + let emojiLabel = UILabel() + emojiLabel.text = emoji.rawValue + emojiLabel.font = .systemFont(ofSize: Values.verySmallFontSize) + + container.addSubview(emojiLabel) + emojiLabel.center(in: container) + + addSubview(container) + container.pin([ UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: self) + container.pin(.right, to: .right, of: self, withInset: -rightMargin) + rightMargin += margin + } + + set(.width, to: rightMargin - margin + size) + } +} diff --git a/Session/Conversations/Message Cells/InfoMessageCell.swift b/Session/Conversations/Message Cells/InfoMessageCell.swift index 4e5b2ab94..b7b56a079 100644 --- a/Session/Conversations/Message Cells/InfoMessageCell.swift +++ b/Session/Conversations/Message Cells/InfoMessageCell.swift @@ -52,7 +52,13 @@ final class InfoMessageCell: MessageCell { // MARK: - Updating - override func update(with cellViewModel: MessageViewModel, mediaCache: NSCache, playbackInfo: ConversationViewModel.PlaybackInfo?, lastSearchText: String?) { + override func update( + with cellViewModel: MessageViewModel, + mediaCache: NSCache, + playbackInfo: ConversationViewModel.PlaybackInfo?, + showExpandedReactions: Bool, + lastSearchText: String? + ) { guard cellViewModel.variant.isInfoMessage else { return } self.viewModel = cellViewModel diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index ef4155580..785abe7d8 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -43,7 +43,13 @@ public class MessageCell: UITableViewCell { // MARK: - Updating - func update(with cellViewModel: MessageViewModel, mediaCache: NSCache, playbackInfo: ConversationViewModel.PlaybackInfo?, lastSearchText: String?) { + func update( + with cellViewModel: MessageViewModel, + mediaCache: NSCache, + playbackInfo: ConversationViewModel.PlaybackInfo?, + showExpandedReactions: Bool, + lastSearchText: String? + ) { preconditionFailure("Must be overridden by subclasses.") } @@ -75,7 +81,7 @@ public class MessageCell: UITableViewCell { // MARK: - MessageCellDelegate -protocol MessageCellDelegate: AnyObject { +protocol MessageCellDelegate: ReactionDelegate { func handleItemLongPressed(_ cellViewModel: MessageViewModel) func handleItemTapped(_ cellViewModel: MessageViewModel, gestureRecognizer: UITapGestureRecognizer) func handleItemDoubleTapped(_ cellViewModel: MessageViewModel) @@ -84,4 +90,6 @@ protocol MessageCellDelegate: AnyObject { func handleReplyButtonTapped(for cellViewModel: MessageViewModel) func showUserDetails(for profile: Profile) func startThread(with sessionId: String, openGroupServer: String?, openGroupPublicKey: String?) + func showReactionList(_ cellViewModel: MessageViewModel, selectedReaction: EmojiWithSkinTones?) + func needsLayout(for cellViewModel: MessageViewModel, expandingReactions: Bool) } diff --git a/Session/Conversations/Message Cells/TypingIndicatorCell.swift b/Session/Conversations/Message Cells/TypingIndicatorCell.swift index 0b40253c2..b4fcc38f8 100644 --- a/Session/Conversations/Message Cells/TypingIndicatorCell.swift +++ b/Session/Conversations/Message Cells/TypingIndicatorCell.swift @@ -39,7 +39,13 @@ final class TypingIndicatorCell: MessageCell { // MARK: - Updating - override func update(with cellViewModel: MessageViewModel, mediaCache: NSCache, playbackInfo: ConversationViewModel.PlaybackInfo?, lastSearchText: String?) { + override func update( + with cellViewModel: MessageViewModel, + mediaCache: NSCache, + playbackInfo: ConversationViewModel.PlaybackInfo?, + showExpandedReactions: Bool, + lastSearchText: String? + ) { guard cellViewModel.cellType == .typingIndicator else { return } self.viewModel = cellViewModel diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index ccb099d8e..f7b23ca19 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -5,12 +5,13 @@ import SignalUtilitiesKit import SessionUtilitiesKit import SessionMessagingKit -final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDelegate { +final class VisibleMessageCell: MessageCell, TappableLabelDelegate { + private var isHandlingLongPress: Bool = false private var unloadContent: (() -> Void)? private var previousX: CGFloat = 0 var albumView: MediaAlbumView? - var bodyTextView: UITextView? + var bodyTappableLabel: TappableLabel? var voiceMessageView: VoiceMessageView? var audioStateChanged: ((TimeInterval, Bool) -> ())? @@ -19,14 +20,20 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0) private lazy var profilePictureViewLeftConstraint = profilePictureView.pin(.left, to: .left, of: self, withInset: VisibleMessageCell.groupThreadHSpacing) private lazy var profilePictureViewWidthConstraint = profilePictureView.set(.width, to: Values.verySmallProfilePictureSize) + private lazy var bubbleViewLeftConstraint1 = bubbleView.pin(.left, to: .right, of: profilePictureView, withInset: VisibleMessageCell.groupThreadHSpacing) private lazy var bubbleViewLeftConstraint2 = bubbleView.leftAnchor.constraint(greaterThanOrEqualTo: leftAnchor, constant: VisibleMessageCell.gutterSize) private lazy var bubbleViewTopConstraint = bubbleView.pin(.top, to: .bottom, of: authorLabel, withInset: VisibleMessageCell.authorLabelBottomSpacing) private lazy var bubbleViewRightConstraint1 = bubbleView.pin(.right, to: .right, of: self, withInset: -VisibleMessageCell.contactThreadHSpacing) private lazy var bubbleViewRightConstraint2 = bubbleView.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor, constant: -VisibleMessageCell.gutterSize) - private lazy var messageStatusImageViewTopConstraint = messageStatusImageView.pin(.top, to: .bottom, of: bubbleView, withInset: 0) + + private lazy var reactionContainerViewLeftConstraint = reactionContainerView.pin(.left, to: .left, of: bubbleView) + private lazy var reactionContainerViewRightConstraint = reactionContainerView.pin(.right, to: .right, of: bubbleView) + + private lazy var messageStatusImageViewTopConstraint = messageStatusImageView.pin(.top, to: .bottom, of: reactionContainerView, withInset: 0) private lazy var messageStatusImageViewWidthConstraint = messageStatusImageView.set(.width, to: VisibleMessageCell.messageStatusImageViewSize) private lazy var messageStatusImageViewHeightConstraint = messageStatusImageView.set(.height, to: VisibleMessageCell.messageStatusImageViewSize) + private lazy var timerViewOutgoingMessageConstraint = timerView.pin(.left, to: .left, of: self, withInset: VisibleMessageCell.contactThreadHSpacing) private lazy var timerViewIncomingMessageConstraint = timerView.pin(.right, to: .right, of: self, withInset: -VisibleMessageCell.contactThreadHSpacing) @@ -44,7 +51,8 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel profilePictureView, replyButton, timerView, - messageStatusImageView + messageStatusImageView, + reactionContainerView ] private lazy var profilePictureView: ProfilePictureView = { @@ -80,7 +88,8 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel }() private lazy var snContentView = UIView() - + private lazy var reactionContainerView = ReactionContainerView() + internal lazy var messageStatusImageView: UIImageView = { let result = UIImageView() result.contentMode = .scaleAspectFit @@ -161,7 +170,6 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel addSubview(profilePictureView) profilePictureViewLeftConstraint.isActive = true profilePictureViewWidthConstraint.isActive = true - profilePictureView.pin(.bottom, to: .bottom, of: self, withInset: -1) // Moderator icon image view moderatorIconImageView.set(.width, to: 20) @@ -178,6 +186,7 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel bubbleViewLeftConstraint1.isActive = true bubbleViewTopConstraint.isActive = true bubbleViewRightConstraint1.isActive = true + bubbleView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: -1) bubbleBackgroundView.pin(to: bubbleView) // Timer view @@ -189,6 +198,11 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel bubbleView.addSubview(snContentView) snContentView.pin(to: bubbleView) + // Reaction view + addSubview(reactionContainerView) + reactionContainerView.pin(.top, to: .bottom, of: bubbleView, withInset: Values.verySmallSpacing) + reactionContainerViewLeftConstraint.isActive = true + // Message status image view addSubview(messageStatusImageView) messageStatusImageViewTopConstraint.isActive = true @@ -228,6 +242,7 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel with cellViewModel: MessageViewModel, mediaCache: NSCache, playbackInfo: ConversationViewModel.PlaybackInfo?, + showExpandedReactions: Bool, lastSearchText: String? ) { self.viewModel = cellViewModel @@ -280,6 +295,12 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel lastSearchText: lastSearchText ) + // Reaction view + reactionContainerView.isHidden = (cellViewModel.reactionInfo?.isEmpty == true) + reactionContainerViewLeftConstraint.isActive = (cellViewModel.variant == .standardIncoming) + reactionContainerViewRightConstraint.isActive = (cellViewModel.variant == .standardOutgoing) + populateReaction(for: cellViewModel, showExpandedReactions: showExpandedReactions) + // Date break headerViewTopConstraint.constant = (shouldInsetHeader ? Values.mediumSpacing : 1) headerView.subviews.forEach { $0.removeFromSuperview() } @@ -389,7 +410,7 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel snContentView.subviews.forEach { $0.removeFromSuperview() } albumView = nil - bodyTextView = nil + bodyTappableLabel = nil // Handle the deleted state first (it's much simpler than the others) guard cellViewModel.variant != .standardIncomingDeleted else { @@ -431,7 +452,7 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel ) snContentView.addSubview(linkPreviewView) linkPreviewView.pin(to: snContentView) - self.bodyTextView = linkPreviewView.bodyTextView + self.bodyTappableLabel = linkPreviewView.bodyTappableLabel case .openGroupInvitation: let openGroupInvitationView: OpenGroupInvitationView = OpenGroupInvitationView( @@ -474,15 +495,15 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel } // Body text view - let bodyTextView = VisibleMessageCell.getBodyTextView( + let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel( for: cellViewModel, with: maxWidth, textColor: bodyLabelTextColor, searchText: lastSearchText, delegate: self ) - self.bodyTextView = bodyTextView - stackView.addArrangedSubview(bodyTextView) + self.bodyTappableLabel = bodyTappableLabel + stackView.addArrangedSubview(bodyTappableLabel) // Constraints snContentView.addSubview(stackView) @@ -516,7 +537,7 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel if let body: String = cellViewModel.body, !body.isEmpty { let inset: CGFloat = 12 let maxWidth: CGFloat = (size.width - (2 * inset)) - let bodyTextView = VisibleMessageCell.getBodyTextView( + let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel( for: cellViewModel, with: maxWidth, textColor: bodyLabelTextColor, @@ -524,8 +545,8 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel delegate: self ) - self.bodyTextView = bodyTextView - stackView.addArrangedSubview(UIView(wrapping: bodyTextView, withInsets: UIEdgeInsets(top: 0, left: inset, bottom: inset, right: inset))) + self.bodyTappableLabel = bodyTappableLabel + stackView.addArrangedSubview(UIView(wrapping: bodyTappableLabel, withInsets: UIEdgeInsets(top: 0, left: inset, bottom: inset, right: inset))) } unloadContent = { albumView.unloadMedia() } @@ -568,7 +589,7 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel // Body text view if let body: String = cellViewModel.body, !body.isEmpty { // delegate should always be set at this point - let bodyTextView = VisibleMessageCell.getBodyTextView( + let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel( for: cellViewModel, with: maxWidth, textColor: bodyLabelTextColor, @@ -576,8 +597,8 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel delegate: self ) - self.bodyTextView = bodyTextView - stackView.addArrangedSubview(bodyTextView) + self.bodyTappableLabel = bodyTappableLabel + stackView.addArrangedSubview(bodyTappableLabel) } // Constraints @@ -585,7 +606,48 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel stackView.pin(to: snContentView, withInset: inset) } } - + + private func populateReaction(for cellViewModel: MessageViewModel, showExpandedReactions: Bool) { + let reactions: OrderedDictionary = (cellViewModel.reactionInfo ?? []) + .reduce(into: OrderedDictionary()) { result, reactionInfo in + guard let emoji: EmojiWithSkinTones = EmojiWithSkinTones(rawValue: reactionInfo.reaction.emoji) else { + return + } + + let isSelfSend: Bool = (reactionInfo.reaction.authorId == cellViewModel.currentUserPublicKey) + + if let value: ReactionViewModel = result.value(forKey: emoji) { + result.replace( + key: emoji, + value: ReactionViewModel( + emoji: emoji, + number: (value.number + Int(reactionInfo.reaction.count)), + showBorder: (value.showBorder || isSelfSend) + ) + ) + } + else { + result.append( + key: emoji, + value: ReactionViewModel( + emoji: emoji, + number: Int(reactionInfo.reaction.count), + showBorder: isSelfSend + ) + ) + } + } + + reactionContainerView.showingAllReactions = showExpandedReactions + reactionContainerView.update( + reactions.orderedValues, + showNumbers: ( + cellViewModel.threadVariant == .closedGroup || + cellViewModel.threadVariant == .openGroup + ) + ) + } + override func layoutSubviews() { super.layoutSubviews() updateBubbleViewCorners() @@ -638,10 +700,10 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel // MARK: - Interaction override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if let bodyTextView = bodyTextView { - let pointInBodyTextViewCoordinates = convert(point, to: bodyTextView) - if bodyTextView.bounds.contains(pointInBodyTextViewCoordinates) { - return bodyTextView + if let bodyTappableLabel = bodyTappableLabel { + let btIngetBodyTappableLabelCoordinates = convert(point, to: bodyTappableLabel) + if bodyTappableLabel.bounds.contains(btIngetBodyTappableLabelCoordinates) { + return bodyTappableLabel } } return super.hitTest(point, with: event) @@ -687,11 +749,31 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel ) } } - - @objc func handleLongPress() { - guard let cellViewModel: MessageViewModel = self.viewModel else { return } + + @objc func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { + if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { + isHandlingLongPress = false + return + } + guard !isHandlingLongPress, let cellViewModel: MessageViewModel = self.viewModel else { return } - delegate?.handleItemLongPressed(cellViewModel) + let location = gestureRecognizer.location(in: self) + + if reactionContainerView.frame.contains(location) { + let convertedLocation = reactionContainerView.convert(location, from: self) + + for reactionView in reactionContainerView.reactionViews { + if reactionContainerView.convert(reactionView.frame, from: reactionView.superview).contains(convertedLocation) { + delegate?.showReactionList(cellViewModel, selectedReaction: reactionView.viewModel.emoji) + break + } + } + } + else { + delegate?.handleItemLongPressed(cellViewModel) + } + + isHandlingLongPress = true } @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { @@ -722,6 +804,32 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel UIImpactFeedbackGenerator(style: .heavy).impactOccurred() reply() } + else if reactionContainerView.frame.contains(location) { + let convertedLocation = reactionContainerView.convert(location, from: self) + + for reactionView in reactionContainerView.reactionViews { + if reactionContainerView.convert(reactionView.frame, from: reactionView.superview).contains(convertedLocation) { + + if reactionView.viewModel.showBorder { + delegate?.removeReact(cellViewModel, for: reactionView.viewModel.emoji) + } + else { + delegate?.react(cellViewModel, with: reactionView.viewModel.emoji) + } + return + } + } + + if let expandButton = reactionContainerView.expandButton, expandButton.frame.contains(convertedLocation) { + reactionContainerView.showAllEmojis() + delegate?.needsLayout(for: cellViewModel, expandingReactions: true) + } + + if reactionContainerView.collapseButton.frame.contains(convertedLocation) { + reactionContainerView.showLessEmojis() + delegate?.needsLayout(for: cellViewModel, expandingReactions: false) + } + } else if bubbleView.frame.contains(location) { delegate?.handleItemTapped(cellViewModel, gestureRecognizer: gestureRecognizer) } @@ -770,20 +878,11 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel default: break } } - - func textView(_ textView: UITextView, shouldInteractWith url: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { - delegate?.openUrl(url.absoluteString) - return false + + func tapableLabel(_ label: TappableLabel, didTapUrl url: String, atRange range: NSRange) { + delegate?.openUrl(url) } - func textViewDidChangeSelection(_ textView: UITextView) { - // Note: We can't just set 'isSelectable' to false otherwise the link detection/selection - // stops working (do a null check to avoid an infinite loop on older iOS versions) - if textView.selectedTextRange != nil { - textView.selectedTextRange = nil - } - } - private func resetReply() { UIView.animate(withDuration: 0.25) { [weak self] in self?.viewsToMoveForReply.forEach { $0.transform = .identity } @@ -801,19 +900,7 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel // MARK: - Convenience private func getCornersToRound() -> UIRectCorner { - guard viewModel?.isOnlyMessageInCluster == false else { return .allCorners } - - let direction: Direction = (viewModel?.variant == .standardOutgoing ? .outgoing : .incoming) - - switch (viewModel?.positionInCluster, direction) { - case (.top, .outgoing): return [ .bottomLeft, .topLeft, .topRight ] - case (.middle, .outgoing): return [ .bottomLeft, .topLeft ] - case (.bottom, .outgoing): return [ .bottomRight, .bottomLeft, .topLeft ] - case (.top, .incoming): return [ .topLeft, .topRight, .bottomRight ] - case (.middle, .incoming): return [ .topRight, .bottomRight ] - case (.bottom, .incoming): return [ .topRight, .bottomRight, .bottomLeft ] - case (.none, _): return .allCorners - } + return .allCorners } private func getCornerMask(from rectCorner: UIRectCorner) -> CACornerMask { @@ -935,24 +1022,16 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel default: preconditionFailure() } } - - static func getBodyTextView( + + static func getBodyTappableLabel( for cellViewModel: MessageViewModel, with availableWidth: CGFloat, textColor: UIColor, searchText: String?, - delegate: (UITextViewDelegate & BodyTextViewDelegate)? - ) -> UITextView { - // Take care of: - // â€ĸ Highlighting mentions - // â€ĸ Linkification - // â€ĸ Highlighting search results - // - // Note: We can't just set 'isSelectable' to false otherwise the link detection/selection - // stops working + delegate: TappableLabelDelegate? + ) -> TappableLabel { + let result = TappableLabel() let isOutgoing: Bool = (cellViewModel.variant == .standardOutgoing) - let result: BodyTextView = BodyTextView(snDelegate: delegate) - result.isEditable = false let attributedText: NSMutableAttributedString = NSMutableAttributedString( attributedString: MentionUtilities.highlightMentions( @@ -968,6 +1047,55 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel ) ) + // Custom handle links + let links: [String: NSRange] = { + guard + let body: String = cellViewModel.body, + let detector: NSDataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + else { return [:] } + + var links: [String: NSRange] = [:] + let matches = detector.matches( + in: body, + options: [], + range: NSRange(location: 0, length: body.count) + ) + + for match in matches { + guard let matchURL = match.url else { continue } + + /// If the URL entered didn't have a scheme it will default to 'http', we want to catch this and + /// set the scheme to 'https' instead as we don't load previews for 'http' so this will result + /// in more previews actually getting loaded without forcing the user to enter 'https://' before + /// every URL they enter + let urlString: String = (matchURL.absoluteString == "http://\(body)" ? + "https://\(body)" : + matchURL.absoluteString + ) + + if URL(string: urlString) != nil { + links[urlString] = (body as NSString).range(of: urlString) + } + } + + return links + }() + + for (urlString, range) in links { + guard let url: URL = URL(string: urlString) else { continue } + + attributedText.addAttributes( + [ + .font: UIFont.systemFont(ofSize: getFontSize(for: cellViewModel)), + .foregroundColor: textColor, + .underlineColor: textColor, + .underlineStyle: NSUnderlineStyle.single.rawValue, + .attachment: url + ], + range: range + ) + } + // If there is a valid search term then highlight each part that matched if let searchText = searchText, searchText.count >= ConversationSearchController.minimumSearchTextLength { let normalizedBody: String = attributedText.string.lowercased() @@ -998,19 +1126,10 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel } result.attributedText = attributedText - result.dataDetectorTypes = .link result.backgroundColor = .clear result.isOpaque = false - result.textContainerInset = UIEdgeInsets.zero - result.contentInset = UIEdgeInsets.zero - result.textContainer.lineFragmentPadding = 0 - result.isScrollEnabled = false result.isUserInteractionEnabled = true result.delegate = delegate - result.linkTextAttributes = [ - .foregroundColor: textColor, - .underlineStyle: NSUnderlineStyle.single.rawValue - ] let availableSpace = CGSize(width: availableWidth, height: .greatestFiniteMagnitude) let size = result.sizeThatFits(availableSpace) diff --git a/Session/Conversations/Views & Modals/BodyTextView.swift b/Session/Conversations/Views & Modals/BodyTextView.swift deleted file mode 100644 index 358333594..000000000 --- a/Session/Conversations/Views & Modals/BodyTextView.swift +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright Š 2022 Rangeproof Pty Ltd. All rights reserved. - -import UIKit - -// Requirements: -// â€ĸ Links should show up properly and be tappable -// â€ĸ Text should * not * be selectable (this is handled via the 'textViewDidChangeSelection(_:)' -// delegate method) -// â€ĸ The long press interaction that shows the context menu should still work -final class BodyTextView: UITextView { - private let snDelegate: BodyTextViewDelegate? - private let highlightedMentionBackgroundView: HighlightMentionBackgroundView = HighlightMentionBackgroundView() - - override var attributedText: NSAttributedString! { - didSet { - guard attributedText != nil else { return } - - highlightedMentionBackgroundView.maxPadding = highlightedMentionBackgroundView - .calculateMaxPadding(for: attributedText) - highlightedMentionBackgroundView.frame = self.bounds.insetBy( - dx: -highlightedMentionBackgroundView.maxPadding, - dy: -highlightedMentionBackgroundView.maxPadding - ) - } - } - - init(snDelegate: BodyTextViewDelegate?) { - self.snDelegate = snDelegate - - super.init(frame: CGRect.zero, textContainer: nil) - - self.clipsToBounds = false // Needed for the 'HighlightMentionBackgroundView' - addSubview(highlightedMentionBackgroundView) - - setUpGestureRecognizers() - } - - override init(frame: CGRect, textContainer: NSTextContainer?) { - preconditionFailure("Use init(snDelegate:) instead.") - } - - required init?(coder: NSCoder) { - preconditionFailure("Use init(snDelegate:) instead.") - } - - private func setUpGestureRecognizers() { - let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) - addGestureRecognizer(longPressGestureRecognizer) - let doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap)) - doubleTapGestureRecognizer.numberOfTapsRequired = 2 - addGestureRecognizer(doubleTapGestureRecognizer) - } - - @objc private func handleLongPress() { - snDelegate?.handleLongPress() - } - - @objc private func handleDoubleTap() { - // Do nothing - } - - override func layoutSubviews() { - super.layoutSubviews() - - highlightedMentionBackgroundView.frame = self.bounds.insetBy( - dx: -highlightedMentionBackgroundView.maxPadding, - dy: -highlightedMentionBackgroundView.maxPadding - ) - } -} - -protocol BodyTextViewDelegate { - - func handleLongPress() -} diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift new file mode 100644 index 000000000..ab501de58 --- /dev/null +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -0,0 +1,588 @@ +// Copyright Š 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import DifferenceKit +import SessionUIKit +import SessionMessagingKit +import SignalUtilitiesKit + +final class ReactionListSheet: BaseVC { + public struct ReactionSummary: Hashable, Differentiable { + let emoji: EmojiWithSkinTones + let number: Int + let isSelected: Bool + + var description: String { + return "\(emoji.rawValue) ¡ \(number)" + } + } + + private let interactionId: Int64 + private let onDismiss: (() -> ())? + private var messageViewModel: MessageViewModel = MessageViewModel() + private var reactionSummaries: [ReactionSummary] = [] + private var selectedReactionUserList: [MessageViewModel.ReactionInfo] = [] + private var lastSelectedReactionIndex: Int = 0 + public var delegate: ReactionDelegate? + + // MARK: - UI + + private lazy var contentView: UIView = { + let result: UIView = UIView() + result.backgroundColor = Colors.modalBackground + + let line: UIView = UIView() + line.backgroundColor = Colors.border.withAlphaComponent(0.5) + result.addSubview(line) + + line.set(.height, to: 0.5) + line.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: result) + + return result + }() + + private lazy var layout: UICollectionViewFlowLayout = { + let result: UICollectionViewFlowLayout = UICollectionViewFlowLayout() + result.scrollDirection = .horizontal + result.sectionInset = UIEdgeInsets( + top: 0, + leading: Values.smallSpacing, + bottom: 0, + trailing: Values.smallSpacing + ) + result.minimumLineSpacing = Values.smallSpacing + result.minimumInteritemSpacing = Values.smallSpacing + result.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + + return result + }() + + private lazy var reactionContainer: UICollectionView = { + let result: UICollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) + result.register(view: Cell.self) + result.set(.height, to: 48) + result.backgroundColor = .clear + result.isScrollEnabled = true + result.showsHorizontalScrollIndicator = false + result.dataSource = self + result.delegate = self + + return result + }() + + private lazy var detailInfoLabel: UILabel = { + let result: UILabel = UILabel() + result.font = .systemFont(ofSize: Values.mediumFontSize) + result.textColor = Colors.grey.withAlphaComponent(0.8) + result.set(.height, to: 32) + + return result + }() + + private lazy var clearAllButton: Button = { + let result: Button = Button(style: .destructiveOutline, size: .small) + result.translatesAutoresizingMaskIntoConstraints = false + result.setTitle("MESSAGE_REQUESTS_CLEAR_ALL".localized(), for: .normal) + result.addTarget(self, action: #selector(clearAllTapped), for: .touchUpInside) + result.layer.borderWidth = 0 + result.isHidden = true + + return result + }() + + private lazy var userListView: UITableView = { + let result: UITableView = UITableView() + result.dataSource = self + result.delegate = self + result.register(view: UserCell.self) + result.register(view: FooterCell.self) + result.separatorStyle = .none + result.backgroundColor = .clear + result.showsVerticalScrollIndicator = false + + return result + }() + + // MARK: - Lifecycle + + init(for interactionId: Int64, onDismiss: (() -> ())? = nil) { + self.interactionId = interactionId + self.onDismiss = onDismiss + + super.init(nibName: nil, bundle: nil) + } + + override init(nibName: String?, bundle: Bundle?) { + preconditionFailure("Use init(for:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(for:) instead.") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .clear + + let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(close)) + swipeGestureRecognizer.direction = .down + view.addGestureRecognizer(swipeGestureRecognizer) + + setUpViewHierarchy() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + reactionContainer.scrollToItem( + at: IndexPath(item: lastSelectedReactionIndex, section: 0), + at: .centeredHorizontally, + animated: false + ) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + self.onDismiss?() + } + + private func setUpViewHierarchy() { + view.addSubview(contentView) + contentView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.bottom ], to: view) + // Emoji collectionView height + seleted emoji detail height + 5 × user cell height + footer cell height + bottom safe area inset + let contentViewHeight: CGFloat = 100 + 5 * 65 + 45 + UIApplication.shared.keyWindow!.safeAreaInsets.bottom + contentView.set(.height, to: contentViewHeight) + populateContentView() + } + + private func populateContentView() { + // Reactions container + contentView.addSubview(reactionContainer) + reactionContainer.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: contentView) + reactionContainer.pin(.top, to: .top, of: contentView, withInset: Values.verySmallSpacing) + + // Seperator + let seperator = UIView() + seperator.backgroundColor = Colors.border.withAlphaComponent(0.1) + seperator.set(.height, to: 0.5) + contentView.addSubview(seperator) + seperator.pin(.leading, to: .leading, of: contentView, withInset: Values.smallSpacing) + seperator.pin(.trailing, to: .trailing, of: contentView, withInset: -Values.smallSpacing) + seperator.pin(.top, to: .bottom, of: reactionContainer, withInset: Values.verySmallSpacing) + + // Detail info & clear all + let stackView = UIStackView(arrangedSubviews: [ detailInfoLabel, clearAllButton ]) + contentView.addSubview(stackView) + stackView.pin(.top, to: .bottom, of: seperator, withInset: Values.smallSpacing) + stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.mediumSpacing) + stackView.pin(.trailing, to: .trailing, of: contentView, withInset: -Values.mediumSpacing) + + // Line + let line = UIView() + line.set(.height, to: 0.5) + line.backgroundColor = Colors.border.withAlphaComponent(0.5) + contentView.addSubview(line) + line.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: contentView) + line.pin(.top, to: .bottom, of: stackView, withInset: Values.smallSpacing) + + // Reactor list + contentView.addSubview(userListView) + userListView.pin([ UIView.HorizontalEdge.trailing, UIView.HorizontalEdge.leading, UIView.VerticalEdge.bottom ], to: contentView) + userListView.pin(.top, to: .bottom, of: line, withInset: 0) + } + + // MARK: - Content + + public func handleInteractionUpdates( + _ allMessages: [MessageViewModel], + selectedReaction: EmojiWithSkinTones? = nil, + updatedReactionIndex: Int? = nil, + initialLoad: Bool = false, + shouldShowClearAllButton: Bool = false + ) { + guard let cellViewModel: MessageViewModel = allMessages.first(where: { $0.id == self.interactionId }) else { + return + } + + // If we have no more reactions (eg. the user removed the last one) then closed the list sheet + guard cellViewModel.reactionInfo?.isEmpty == false else { + close() + return + } + + // Generated the updated data + let updatedReactionInfo: OrderedDictionary = (cellViewModel.reactionInfo ?? []) + .reduce(into: OrderedDictionary()) { + result, reactionInfo in + guard let emoji: EmojiWithSkinTones = EmojiWithSkinTones(rawValue: reactionInfo.reaction.emoji) else { + return + } + + guard var updatedValue: [MessageViewModel.ReactionInfo] = result.value(forKey: emoji) else { + result.append(key: emoji, value: [reactionInfo]) + return + } + + if reactionInfo.reaction.authorId == cellViewModel.currentUserPublicKey { + updatedValue.insert(reactionInfo, at: 0) + } + else { + updatedValue.append(reactionInfo) + } + + result.replace(key: emoji, value: updatedValue) + } + let oldSelectedReactionIndex: Int = self.lastSelectedReactionIndex + let updatedSelectedReactionIndex: Int = updatedReactionIndex + .defaulting( + to: { + // If we explicitly provided a 'selectedReaction' value then try to use that + if selectedReaction != nil, let targetIndex: Int = updatedReactionInfo.orderedKeys.firstIndex(where: { $0 == selectedReaction }) { + return targetIndex + } + + // Otherwise try to maintain the index of the currently selected index + guard + !self.reactionSummaries.isEmpty, + let emoji: EmojiWithSkinTones = self.reactionSummaries[safe: oldSelectedReactionIndex]?.emoji, + let targetIndex: Int = updatedReactionInfo.orderedKeys.firstIndex(of: emoji) + else { return 0 } + + return targetIndex + }() + ) + let updatedSummaries: [ReactionSummary] = updatedReactionInfo + .orderedKeys + .enumerated() + .map { index, emoji in + ReactionSummary( + emoji: emoji, + number: updatedReactionInfo.value(forKey: emoji) + .defaulting(to: []) + .map { Int($0.reaction.count) } + .reduce(0, +), + isSelected: (index == updatedSelectedReactionIndex) + ) + } + + // Update the general UI + self.detailInfoLabel.text = updatedSummaries[safe: updatedSelectedReactionIndex]?.description + + // Update general properties + self.messageViewModel = cellViewModel + self.lastSelectedReactionIndex = updatedSelectedReactionIndex + + // Ensure the first load or a load when returning from a child screen runs without animations (if + // we don't do this the cells will animate in from a frame of CGRect.zero or have a buggy transition) + guard !initialLoad else { + self.reactionSummaries = updatedSummaries + self.selectedReactionUserList = updatedReactionInfo + .orderedKeys[safe: updatedSelectedReactionIndex] + .map { updatedReactionInfo.value(forKey: $0) } + .defaulting(to: []) + + // Update clear all button visibility + self.clearAllButton.isHidden = !shouldShowClearAllButton + + UIView.performWithoutAnimation { + self.reactionContainer.reloadData() + self.userListView.reloadData() + } + return + } + + // Update the collection view content + let collectionViewChangeset: StagedChangeset<[ReactionSummary]> = StagedChangeset( + source: self.reactionSummaries, + target: updatedSummaries + ) + + // If there are changes then we want to reload both the collection and table views + self.reactionContainer.reload( + using: collectionViewChangeset, + interrupt: { $0.changeCount > 1 } + ) { [weak self] updatedData in + self?.reactionSummaries = updatedData + } + + // If we changed the selected index then no need to reload the changes + guard + oldSelectedReactionIndex == updatedSelectedReactionIndex && + self.reactionSummaries[safe: oldSelectedReactionIndex]?.emoji == updatedSummaries[safe: updatedSelectedReactionIndex]?.emoji + else { + self.selectedReactionUserList = updatedReactionInfo + .orderedKeys[safe: updatedSelectedReactionIndex] + .map { updatedReactionInfo.value(forKey: $0) } + .defaulting(to: []) + self.userListView.reloadData() + return + } + + let tableChangeset: StagedChangeset<[MessageViewModel.ReactionInfo]> = StagedChangeset( + source: self.selectedReactionUserList, + target: updatedReactionInfo + .orderedKeys[safe: updatedSelectedReactionIndex] + .map { updatedReactionInfo.value(forKey: $0) } + .defaulting(to: []) + ) + + self.userListView.reload( + using: tableChangeset, + deleteSectionsAnimation: .none, + insertSectionsAnimation: .none, + reloadSectionsAnimation: .none, + deleteRowsAnimation: .none, + insertRowsAnimation: .none, + reloadRowsAnimation: .none, + interrupt: { [weak self] changeset in + /// This is the case where there were 6 reactors in total and locally we only have 5 including current user, + /// and current user remove the reaction. There would be 4 reactors locally and we need to show more + /// reactors cell at this moment. After update from sogs, we'll get the all 5 reactors and update the table + /// with 5 reactors and not showing the more reactors cell. + changeset.elementInserted.count == 1 && self?.selectedReactionUserList.count == 4 || + /// This is the case where there were 5 reactors without current user, and current user reacted. Before we got + /// the update from sogs, we'll have 6 reactors locally and not showing the more reactors cell. After the update, + /// we'll need to update the table and show 5 reactors with the more reactors cell. + changeset.elementDeleted.count == 1 && self?.selectedReactionUserList.count == 6 || + /// To many changes to make + changeset.changeCount > 100 + } + ) { [weak self] updatedData in + self?.selectedReactionUserList = updatedData + } + } + + // MARK: - Interaction + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + guard let touch: UITouch = touches.first, contentView.frame.contains(touch.location(in: view)) else { + close() + return + } + + super.touchesBegan(touches, with: event) + } + + @objc func close() { + dismiss(animated: true, completion: nil) + } + + @objc private func clearAllTapped() { + guard let selectedReaction: EmojiWithSkinTones = self.reactionSummaries.first(where: { $0.isSelected })?.emoji else { return } + + delegate?.removeAllReactions(messageViewModel, for: selectedReaction.rawValue) + } +} + +// MARK: - UICollectionView + +extension ReactionListSheet: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + // MARK: Data Source + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.reactionSummaries.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell: Cell = collectionView.dequeue(type: Cell.self, for: indexPath) + let summary: ReactionSummary = self.reactionSummaries[indexPath.item] + + cell.update( + with: summary.emoji.rawValue, + count: summary.number, + isCurrentSelection: summary.isSelected + ) + + return cell + } + + // MARK: Interaction + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + self.handleInteractionUpdates([messageViewModel], updatedReactionIndex: indexPath.item) + } +} + +// MARK: - UITableViewDelegate & UITableViewDataSource + +extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let moreReactorCount = self.reactionSummaries[lastSelectedReactionIndex].number - self.selectedReactionUserList.count + return moreReactorCount > 0 ? self.selectedReactionUserList.count + 1 : self.selectedReactionUserList.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard indexPath.row < self.selectedReactionUserList.count else { + let moreReactorCount = self.reactionSummaries[lastSelectedReactionIndex].number - self.selectedReactionUserList.count + let footerCell: FooterCell = tableView.dequeue(type: FooterCell.self, for: indexPath) + footerCell.update( + moreReactorCount: moreReactorCount, + emoji: self.reactionSummaries[lastSelectedReactionIndex].emoji.rawValue + ) + footerCell.selectionStyle = .none + + return footerCell + } + + let cell: UserCell = tableView.dequeue(type: UserCell.self, for: indexPath) + let cellViewModel: MessageViewModel.ReactionInfo = self.selectedReactionUserList[indexPath.row] + cell.update( + with: cellViewModel.reaction.authorId, + profile: cellViewModel.profile, + isZombie: false, + mediumFont: true, + accessory: (cellViewModel.reaction.authorId == self.messageViewModel.currentUserPublicKey ? + .x : + .none + ) + ) + + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + guard indexPath.row < self.selectedReactionUserList.count else { return } + + let cellViewModel: MessageViewModel.ReactionInfo = self.selectedReactionUserList[indexPath.row] + + guard + let selectedReaction: EmojiWithSkinTones = self.reactionSummaries + .first(where: { $0.isSelected })? + .emoji, + selectedReaction.rawValue == cellViewModel.reaction.emoji, + cellViewModel.reaction.authorId == self.messageViewModel.currentUserPublicKey + else { return } + + delegate?.removeReact(self.messageViewModel, for: selectedReaction) + } +} + +// MARK: - Cell + +extension ReactionListSheet { + fileprivate final class Cell: UICollectionViewCell { + // MARK: - UI + + private static var contentViewHeight: CGFloat = 32 + private static var contentViewCornerRadius: CGFloat { contentViewHeight / 2 } + + private lazy var snContentView: UIView = { + let result = UIView() + result.backgroundColor = Colors.receivedMessageBackground + result.set(.height, to: Cell.contentViewHeight) + result.layer.cornerRadius = Cell.contentViewCornerRadius + return result + }() + + private lazy var emojiLabel: UILabel = { + let result = UILabel() + result.font = .systemFont(ofSize: Values.mediumFontSize) + return result + }() + + private lazy var numberLabel: UILabel = { + let result = UILabel() + result.textColor = Colors.text + result.font = .systemFont(ofSize: Values.mediumFontSize) + + return result + }() + + // MARK: - Initialization + + override init(frame: CGRect) { + super.init(frame: frame) + + setUpViewHierarchy() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + + setUpViewHierarchy() + } + + private func setUpViewHierarchy() { + addSubview(snContentView) + + let stackView = UIStackView(arrangedSubviews: [ emojiLabel, numberLabel ]) + stackView.axis = .horizontal + stackView.alignment = .center + + let spacing = Values.smallSpacing + 2 + stackView.spacing = spacing + stackView.layoutMargins = UIEdgeInsets(top: 0, left: spacing, bottom: 0, right: spacing) + stackView.isLayoutMarginsRelativeArrangement = true + snContentView.addSubview(stackView) + stackView.pin(to: snContentView) + snContentView.pin(to: self) + } + + // MARK: - Content + + fileprivate func update( + with emoji: String, + count: Int, + isCurrentSelection: Bool + ) { + snContentView.addBorder( + with: (isCurrentSelection == true ? Colors.accent : .clear) + ) + + emojiLabel.text = emoji + numberLabel.text = (count < 1000 ? + "\(count)" : + String(format: "%.1fk", Float(count) / 1000) + ) + } + } + + fileprivate final class FooterCell: UITableViewCell { + + private lazy var label: UILabel = { + let result = UILabel() + result.textAlignment = .center + result.font = .systemFont(ofSize: Values.smallFontSize) + result.textColor = Colors.grey.withAlphaComponent(0.8) + return result + }() + + // MARK: - Initialization + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setUpViewHierarchy() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setUpViewHierarchy() + } + + private func setUpViewHierarchy() { + // Background color + backgroundColor = Colors.cellBackground + + contentView.addSubview(label) + label.pin(to: contentView) + label.set(.height, to: 45) + } + + func update(moreReactorCount: Int, emoji: String) { + label.text = (moreReactorCount == 1) ? + String(format: "EMOJI_REACTS_MORE_REACTORS_ONE".localized(), "\(emoji)") : + String(format: "EMOJI_REACTS_MORE_REACTORS_MUTIPLE".localized(), "\(moreReactorCount)" ,"\(emoji)") + } + } +} + +// MARK: - Delegate + +protocol ReactionDelegate: AnyObject { + func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones) + func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones) + func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String) +} diff --git a/Session/Emoji/Emoji+Available.swift b/Session/Emoji/Emoji+Available.swift new file mode 100644 index 000000000..5b17fa05e --- /dev/null +++ b/Session/Emoji/Emoji+Available.swift @@ -0,0 +1,111 @@ +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() + } +} diff --git a/Session/Emoji/Emoji+Category.swift b/Session/Emoji/Emoji+Category.swift new file mode 100644 index 000000000..3b6b8275b --- /dev/null +++ b/Session/Emoji/Emoji+Category.swift @@ -0,0 +1,3776 @@ + +// This file is generated by EmojiGenerator.swift, do not manually edit it. + +extension Emoji { + enum Category: String, CaseIterable, Equatable { + case smileysAndPeople = "Smileys & People" + case animals = "Animals & Nature" + case food = "Food & Drink" + case activities = "Activities" + case travel = "Travel & Places" + case objects = "Objects" + case symbols = "Symbols" + case flags = "Flags" + + var localizedName: String { + switch self { + case .smileysAndPeople: + return NSLocalizedString("EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME", comment: "The name for the emoji category 'Smileys & People'") + case .animals: + return NSLocalizedString("EMOJI_CATEGORY_ANIMALS_NAME", comment: "The name for the emoji category 'Animals & Nature'") + case .food: + return NSLocalizedString("EMOJI_CATEGORY_FOOD_NAME", comment: "The name for the emoji category 'Food & Drink'") + case .activities: + return NSLocalizedString("EMOJI_CATEGORY_ACTIVITIES_NAME", comment: "The name for the emoji category 'Activities'") + case .travel: + return NSLocalizedString("EMOJI_CATEGORY_TRAVEL_NAME", comment: "The name for the emoji category 'Travel & Places'") + case .objects: + return NSLocalizedString("EMOJI_CATEGORY_OBJECTS_NAME", comment: "The name for the emoji category 'Objects'") + case .symbols: + return NSLocalizedString("EMOJI_CATEGORY_SYMBOLS_NAME", comment: "The name for the emoji category 'Symbols'") + case .flags: + return NSLocalizedString("EMOJI_CATEGORY_FLAGS_NAME", comment: "The name for the emoji category 'Flags'") + } + } + + var normalizedEmoji: [Emoji] { + switch self { + case .smileysAndPeople: + return [ + .grinning, + .smiley, + .smile, + .grin, + .laughing, + .sweatSmile, + .rollingOnTheFloorLaughing, + .joy, + .slightlySmilingFace, + .upsideDownFace, + .meltingFace, + .wink, + .blush, + .innocent, + .smilingFaceWith3Hearts, + .heartEyes, + .starStruck, + .kissingHeart, + .kissing, + .relaxed, + .kissingClosedEyes, + .kissingSmilingEyes, + .smilingFaceWithTear, + .yum, + .stuckOutTongue, + .stuckOutTongueWinkingEye, + .zanyFace, + .stuckOutTongueClosedEyes, + .moneyMouthFace, + .huggingFace, + .faceWithHandOverMouth, + .faceWithOpenEyesAndHandOverMouth, + .faceWithPeekingEye, + .shushingFace, + .thinkingFace, + .salutingFace, + .zipperMouthFace, + .faceWithRaisedEyebrow, + .neutralFace, + .expressionless, + .noMouth, + .dottedLineFace, + .faceInClouds, + .smirk, + .unamused, + .faceWithRollingEyes, + .grimacing, + .faceExhaling, + .lyingFace, + .relieved, + .pensive, + .sleepy, + .droolingFace, + .sleeping, + .mask, + .faceWithThermometer, + .faceWithHeadBandage, + .nauseatedFace, + .faceVomiting, + .sneezingFace, + .hotFace, + .coldFace, + .woozyFace, + .dizzyFace, + .faceWithSpiralEyes, + .explodingHead, + .faceWithCowboyHat, + .partyingFace, + .disguisedFace, + .sunglasses, + .nerdFace, + .faceWithMonocle, + .confused, + .faceWithDiagonalMouth, + .worried, + .slightlyFrowningFace, + .whiteFrowningFace, + .openMouth, + .hushed, + .astonished, + .flushed, + .pleadingFace, + .faceHoldingBackTears, + .frowning, + .anguished, + .fearful, + .coldSweat, + .disappointedRelieved, + .cry, + .sob, + .scream, + .confounded, + .persevere, + .disappointed, + .sweat, + .weary, + .tiredFace, + .yawningFace, + .triumph, + .rage, + .angry, + .faceWithSymbolsOnMouth, + .smilingImp, + .imp, + .skull, + .skullAndCrossbones, + .hankey, + .clownFace, + .japaneseOgre, + .japaneseGoblin, + .ghost, + .alien, + .spaceInvader, + .robotFace, + .smileyCat, + .smileCat, + .joyCat, + .heartEyesCat, + .smirkCat, + .kissingCat, + .screamCat, + .cryingCatFace, + .poutingCat, + .seeNoEvil, + .hearNoEvil, + .speakNoEvil, + .kiss, + .loveLetter, + .cupid, + .giftHeart, + .sparklingHeart, + .heartpulse, + .heartbeat, + .revolvingHearts, + .twoHearts, + .heartDecoration, + .heavyHeartExclamationMarkOrnament, + .brokenHeart, + .heartOnFire, + .mendingHeart, + .heart, + .orangeHeart, + .yellowHeart, + .greenHeart, + .blueHeart, + .purpleHeart, + .brownHeart, + .blackHeart, + .whiteHeart, + .oneHundred, + .anger, + .boom, + .dizzy, + .sweatDrops, + .dash, + .hole, + .bomb, + .speechBalloon, + .eyeInSpeechBubble, + .leftSpeechBubble, + .rightAngerBubble, + .thoughtBalloon, + .zzz, + .wave, + .raisedBackOfHand, + .raisedHandWithFingersSplayed, + .hand, + .spockHand, + .rightwardsHand, + .leftwardsHand, + .palmDownHand, + .palmUpHand, + .okHand, + .pinchedFingers, + .pinchingHand, + .v, + .crossedFingers, + .handWithIndexFingerAndThumbCrossed, + .iLoveYouHandSign, + .theHorns, + .callMeHand, + .pointLeft, + .pointRight, + .pointUp2, + .middleFinger, + .pointDown, + .pointUp, + .indexPointingAtTheViewer, + .plusOne, + .negativeOne, + .fist, + .facepunch, + .leftFacingFist, + .rightFacingFist, + .clap, + .raisedHands, + .heartHands, + .openHands, + .palmsUpTogether, + .handshake, + .pray, + .writingHand, + .nailCare, + .selfie, + .muscle, + .mechanicalArm, + .mechanicalLeg, + .leg, + .foot, + .ear, + .earWithHearingAid, + .nose, + .brain, + .anatomicalHeart, + .lungs, + .tooth, + .bone, + .eyes, + .eye, + .tongue, + .lips, + .bitingLip, + .baby, + .child, + .boy, + .girl, + .adult, + .personWithBlondHair, + .man, + .beardedPerson, + .manWithBeard, + .womanWithBeard, + .redHairedMan, + .curlyHairedMan, + .whiteHairedMan, + .baldMan, + .woman, + .redHairedWoman, + .redHairedPerson, + .curlyHairedWoman, + .curlyHairedPerson, + .whiteHairedWoman, + .whiteHairedPerson, + .baldWoman, + .baldPerson, + .blondHairedWoman, + .blondHairedMan, + .olderAdult, + .olderMan, + .olderWoman, + .personFrowning, + .manFrowning, + .womanFrowning, + .personWithPoutingFace, + .manPouting, + .womanPouting, + .noGood, + .manGesturingNo, + .womanGesturingNo, + .okWoman, + .manGesturingOk, + .womanGesturingOk, + .informationDeskPerson, + .manTippingHand, + .womanTippingHand, + .raisingHand, + .manRaisingHand, + .womanRaisingHand, + .deafPerson, + .deafMan, + .deafWoman, + .bow, + .manBowing, + .womanBowing, + .facePalm, + .manFacepalming, + .womanFacepalming, + .shrug, + .manShrugging, + .womanShrugging, + .healthWorker, + .maleDoctor, + .femaleDoctor, + .student, + .maleStudent, + .femaleStudent, + .teacher, + .maleTeacher, + .femaleTeacher, + .judge, + .maleJudge, + .femaleJudge, + .farmer, + .maleFarmer, + .femaleFarmer, + .cook, + .maleCook, + .femaleCook, + .mechanic, + .maleMechanic, + .femaleMechanic, + .factoryWorker, + .maleFactoryWorker, + .femaleFactoryWorker, + .officeWorker, + .maleOfficeWorker, + .femaleOfficeWorker, + .scientist, + .maleScientist, + .femaleScientist, + .technologist, + .maleTechnologist, + .femaleTechnologist, + .singer, + .maleSinger, + .femaleSinger, + .artist, + .maleArtist, + .femaleArtist, + .pilot, + .malePilot, + .femalePilot, + .astronaut, + .maleAstronaut, + .femaleAstronaut, + .firefighter, + .maleFirefighter, + .femaleFirefighter, + .cop, + .malePoliceOfficer, + .femalePoliceOfficer, + .sleuthOrSpy, + .maleDetective, + .femaleDetective, + .guardsman, + .maleGuard, + .femaleGuard, + .ninja, + .constructionWorker, + .maleConstructionWorker, + .femaleConstructionWorker, + .personWithCrown, + .prince, + .princess, + .manWithTurban, + .manWearingTurban, + .womanWearingTurban, + .manWithGuaPiMao, + .personWithHeadscarf, + .personInTuxedo, + .manInTuxedo, + .womanInTuxedo, + .brideWithVeil, + .manWithVeil, + .womanWithVeil, + .pregnantWoman, + .pregnantMan, + .pregnantPerson, + .breastFeeding, + .womanFeedingBaby, + .manFeedingBaby, + .personFeedingBaby, + .angel, + .santa, + .mrsClaus, + .mxClaus, + .superhero, + .maleSuperhero, + .femaleSuperhero, + .supervillain, + .maleSupervillain, + .femaleSupervillain, + .mage, + .maleMage, + .femaleMage, + .fairy, + .maleFairy, + .femaleFairy, + .vampire, + .maleVampire, + .femaleVampire, + .merperson, + .merman, + .mermaid, + .elf, + .maleElf, + .femaleElf, + .genie, + .maleGenie, + .femaleGenie, + .zombie, + .maleZombie, + .femaleZombie, + .troll, + .massage, + .manGettingMassage, + .womanGettingMassage, + .haircut, + .manGettingHaircut, + .womanGettingHaircut, + .walking, + .manWalking, + .womanWalking, + .standingPerson, + .manStanding, + .womanStanding, + .kneelingPerson, + .manKneeling, + .womanKneeling, + .personWithProbingCane, + .manWithProbingCane, + .womanWithProbingCane, + .personInMotorizedWheelchair, + .manInMotorizedWheelchair, + .womanInMotorizedWheelchair, + .personInManualWheelchair, + .manInManualWheelchair, + .womanInManualWheelchair, + .runner, + .manRunning, + .womanRunning, + .dancer, + .manDancing, + .manInBusinessSuitLevitating, + .dancers, + .menWithBunnyEarsPartying, + .womenWithBunnyEarsPartying, + .personInSteamyRoom, + .manInSteamyRoom, + .womanInSteamyRoom, + .personClimbing, + .manClimbing, + .womanClimbing, + .fencer, + .horseRacing, + .skier, + .snowboarder, + .golfer, + .manGolfing, + .womanGolfing, + .surfer, + .manSurfing, + .womanSurfing, + .rowboat, + .manRowingBoat, + .womanRowingBoat, + .swimmer, + .manSwimming, + .womanSwimming, + .personWithBall, + .manBouncingBall, + .womanBouncingBall, + .weightLifter, + .manLiftingWeights, + .womanLiftingWeights, + .bicyclist, + .manBiking, + .womanBiking, + .mountainBicyclist, + .manMountainBiking, + .womanMountainBiking, + .personDoingCartwheel, + .manCartwheeling, + .womanCartwheeling, + .wrestlers, + .manWrestling, + .womanWrestling, + .waterPolo, + .manPlayingWaterPolo, + .womanPlayingWaterPolo, + .handball, + .manPlayingHandball, + .womanPlayingHandball, + .juggling, + .manJuggling, + .womanJuggling, + .personInLotusPosition, + .manInLotusPosition, + .womanInLotusPosition, + .bath, + .sleepingAccommodation, + .peopleHoldingHands, + .twoWomenHoldingHands, + .manAndWomanHoldingHands, + .twoMenHoldingHands, + .personKissPerson, + .womanKissMan, + .manKissMan, + .womanKissWoman, + .personHeartPerson, + .womanHeartMan, + .manHeartMan, + .womanHeartWoman, + .family, + .manWomanBoy, + .manWomanGirl, + .manWomanGirlBoy, + .manWomanBoyBoy, + .manWomanGirlGirl, + .manManBoy, + .manManGirl, + .manManGirlBoy, + .manManBoyBoy, + .manManGirlGirl, + .womanWomanBoy, + .womanWomanGirl, + .womanWomanGirlBoy, + .womanWomanBoyBoy, + .womanWomanGirlGirl, + .manBoy, + .manBoyBoy, + .manGirl, + .manGirlBoy, + .manGirlGirl, + .womanBoy, + .womanBoyBoy, + .womanGirl, + .womanGirlBoy, + .womanGirlGirl, + .speakingHeadInSilhouette, + .bustInSilhouette, + .bustsInSilhouette, + .peopleHugging, + .footprints, + ] + case .animals: + return [ + .monkeyFace, + .monkey, + .gorilla, + .orangutan, + .dog, + .dog2, + .guideDog, + .serviceDog, + .poodle, + .wolf, + .foxFace, + .raccoon, + .cat, + .cat2, + .blackCat, + .lionFace, + .tiger, + .tiger2, + .leopard, + .horse, + .racehorse, + .unicornFace, + .zebraFace, + .deer, + .bison, + .cow, + .ox, + .waterBuffalo, + .cow2, + .pig, + .pig2, + .boar, + .pigNose, + .ram, + .sheep, + .goat, + .dromedaryCamel, + .camel, + .llama, + .giraffeFace, + .elephant, + .mammoth, + .rhinoceros, + .hippopotamus, + .mouse, + .mouse2, + .rat, + .hamster, + .rabbit, + .rabbit2, + .chipmunk, + .beaver, + .hedgehog, + .bat, + .bear, + .polarBear, + .koala, + .pandaFace, + .sloth, + .otter, + .skunk, + .kangaroo, + .badger, + .feet, + .turkey, + .chicken, + .rooster, + .hatchingChick, + .babyChick, + .hatchedChick, + .bird, + .penguin, + .doveOfPeace, + .eagle, + .duck, + .swan, + .owl, + .dodo, + .feather, + .flamingo, + .peacock, + .parrot, + .frog, + .crocodile, + .turtle, + .lizard, + .snake, + .dragonFace, + .dragon, + .sauropod, + .tRex, + .whale, + .whale2, + .dolphin, + .seal, + .fish, + .tropicalFish, + .blowfish, + .shark, + .octopus, + .shell, + .coral, + .snail, + .butterfly, + .bug, + .ant, + .bee, + .beetle, + .ladybug, + .cricket, + .cockroach, + .spider, + .spiderWeb, + .scorpion, + .mosquito, + .fly, + .worm, + .microbe, + .bouquet, + .cherryBlossom, + .whiteFlower, + .lotus, + .rosette, + .rose, + .wiltedFlower, + .hibiscus, + .sunflower, + .blossom, + .tulip, + .seedling, + .pottedPlant, + .evergreenTree, + .deciduousTree, + .palmTree, + .cactus, + .earOfRice, + .herb, + .shamrock, + .fourLeafClover, + .mapleLeaf, + .fallenLeaf, + .leaves, + .emptyNest, + .nestWithEggs, + ] + case .food: + return [ + .grapes, + .melon, + .watermelon, + .tangerine, + .lemon, + .banana, + .pineapple, + .mango, + .apple, + .greenApple, + .pear, + .peach, + .cherries, + .strawberry, + .blueberries, + .kiwifruit, + .tomato, + .olive, + .coconut, + .avocado, + .eggplant, + .potato, + .carrot, + .corn, + .hotPepper, + .bellPepper, + .cucumber, + .leafyGreen, + .broccoli, + .garlic, + .onion, + .mushroom, + .peanuts, + .beans, + .chestnut, + .bread, + .croissant, + .baguetteBread, + .flatbread, + .pretzel, + .bagel, + .pancakes, + .waffle, + .cheeseWedge, + .meatOnBone, + .poultryLeg, + .cutOfMeat, + .bacon, + .hamburger, + .fries, + .pizza, + .hotdog, + .sandwich, + .taco, + .burrito, + .tamale, + .stuffedFlatbread, + .falafel, + .egg, + .friedEgg, + .shallowPanOfFood, + .stew, + .fondue, + .bowlWithSpoon, + .greenSalad, + .popcorn, + .butter, + .salt, + .cannedFood, + .bento, + .riceCracker, + .riceBall, + .rice, + .curry, + .ramen, + .spaghetti, + .sweetPotato, + .oden, + .sushi, + .friedShrimp, + .fishCake, + .moonCake, + .dango, + .dumpling, + .fortuneCookie, + .takeoutBox, + .crab, + .lobster, + .shrimp, + .squid, + .oyster, + .icecream, + .shavedIce, + .iceCream, + .doughnut, + .cookie, + .birthday, + .cake, + .cupcake, + .pie, + .chocolateBar, + .candy, + .lollipop, + .custard, + .honeyPot, + .babyBottle, + .glassOfMilk, + .coffee, + .teapot, + .tea, + .sake, + .champagne, + .wineGlass, + .cocktail, + .tropicalDrink, + .beer, + .beers, + .clinkingGlasses, + .tumblerGlass, + .pouringLiquid, + .cupWithStraw, + .bubbleTea, + .beverageBox, + .mateDrink, + .iceCube, + .chopsticks, + .knifeForkPlate, + .forkAndKnife, + .spoon, + .hocho, + .jar, + .amphora, + ] + case .activities: + return [ + .jackOLantern, + .christmasTree, + .fireworks, + .sparkler, + .firecracker, + .sparkles, + .balloon, + .tada, + .confettiBall, + .tanabataTree, + .bamboo, + .dolls, + .flags, + .windChime, + .riceScene, + .redEnvelope, + .ribbon, + .gift, + .reminderRibbon, + .admissionTickets, + .ticket, + .medal, + .trophy, + .sportsMedal, + .firstPlaceMedal, + .secondPlaceMedal, + .thirdPlaceMedal, + .soccer, + .baseball, + .softball, + .basketball, + .volleyball, + .football, + .rugbyFootball, + .tennis, + .flyingDisc, + .bowling, + .cricketBatAndBall, + .fieldHockeyStickAndBall, + .iceHockeyStickAndPuck, + .lacrosse, + .tableTennisPaddleAndBall, + .badmintonRacquetAndShuttlecock, + .boxingGlove, + .martialArtsUniform, + .goalNet, + .golf, + .iceSkate, + .fishingPoleAndFish, + .divingMask, + .runningShirtWithSash, + .ski, + .sled, + .curlingStone, + .dart, + .yoYo, + .kite, + .eightBall, + .crystalBall, + .magicWand, + .nazarAmulet, + .hamsa, + .videoGame, + .joystick, + .slotMachine, + .gameDie, + .jigsaw, + .teddyBear, + .pinata, + .mirrorBall, + .nestingDolls, + .spades, + .hearts, + .diamonds, + .clubs, + .chessPawn, + .blackJoker, + .mahjong, + .flowerPlayingCards, + .performingArts, + .frameWithPicture, + .art, + .thread, + .sewingNeedle, + .yarn, + .knot, + ] + case .travel: + return [ + .earthAfrica, + .earthAmericas, + .earthAsia, + .globeWithMeridians, + .worldMap, + .japan, + .compass, + .snowCappedMountain, + .mountain, + .volcano, + .mountFuji, + .camping, + .beachWithUmbrella, + .desert, + .desertIsland, + .nationalPark, + .stadium, + .classicalBuilding, + .buildingConstruction, + .bricks, + .rock, + .wood, + .hut, + .houseBuildings, + .derelictHouseBuilding, + .house, + .houseWithGarden, + .office, + .postOffice, + .europeanPostOffice, + .hospital, + .bank, + .hotel, + .loveHotel, + .convenienceStore, + .school, + .departmentStore, + .factory, + .japaneseCastle, + .europeanCastle, + .wedding, + .tokyoTower, + .statueOfLiberty, + .church, + .mosque, + .hinduTemple, + .synagogue, + .shintoShrine, + .kaaba, + .fountain, + .tent, + .foggy, + .nightWithStars, + .cityscape, + .sunriseOverMountains, + .sunrise, + .citySunset, + .citySunrise, + .bridgeAtNight, + .hotsprings, + .carouselHorse, + .playgroundSlide, + .ferrisWheel, + .rollerCoaster, + .barber, + .circusTent, + .steamLocomotive, + .railwayCar, + .bullettrainSide, + .bullettrainFront, + .train2, + .metro, + .lightRail, + .station, + .tram, + .monorail, + .mountainRailway, + .train, + .bus, + .oncomingBus, + .trolleybus, + .minibus, + .ambulance, + .fireEngine, + .policeCar, + .oncomingPoliceCar, + .taxi, + .oncomingTaxi, + .car, + .oncomingAutomobile, + .blueCar, + .pickupTruck, + .truck, + .articulatedLorry, + .tractor, + .racingCar, + .racingMotorcycle, + .motorScooter, + .manualWheelchair, + .motorizedWheelchair, + .autoRickshaw, + .bike, + .scooter, + .skateboard, + .rollerSkate, + .busstop, + .motorway, + .railwayTrack, + .oilDrum, + .fuelpump, + .wheel, + .rotatingLight, + .trafficLight, + .verticalTrafficLight, + .octagonalSign, + .construction, + .anchor, + .ringBuoy, + .boat, + .canoe, + .speedboat, + .passengerShip, + .ferry, + .motorBoat, + .ship, + .airplane, + .smallAirplane, + .airplaneDeparture, + .airplaneArriving, + .parachute, + .seat, + .helicopter, + .suspensionRailway, + .mountainCableway, + .aerialTramway, + .satellite, + .rocket, + .flyingSaucer, + .bellhopBell, + .luggage, + .hourglass, + .hourglassFlowingSand, + .watch, + .alarmClock, + .stopwatch, + .timerClock, + .mantelpieceClock, + .clock12, + .clock1230, + .clock1, + .clock130, + .clock2, + .clock230, + .clock3, + .clock330, + .clock4, + .clock430, + .clock5, + .clock530, + .clock6, + .clock630, + .clock7, + .clock730, + .clock8, + .clock830, + .clock9, + .clock930, + .clock10, + .clock1030, + .clock11, + .clock1130, + .newMoon, + .waxingCrescentMoon, + .firstQuarterMoon, + .moon, + .fullMoon, + .waningGibbousMoon, + .lastQuarterMoon, + .waningCrescentMoon, + .crescentMoon, + .newMoonWithFace, + .firstQuarterMoonWithFace, + .lastQuarterMoonWithFace, + .thermometer, + .sunny, + .fullMoonWithFace, + .sunWithFace, + .ringedPlanet, + .star, + .star2, + .stars, + .milkyWay, + .cloud, + .partlySunny, + .thunderCloudAndRain, + .mostlySunny, + .barelySunny, + .partlySunnyRain, + .rainCloud, + .snowCloud, + .lightning, + .tornado, + .fog, + .windBlowingFace, + .cyclone, + .rainbow, + .closedUmbrella, + .umbrella, + .umbrellaWithRainDrops, + .umbrellaOnGround, + .zap, + .snowflake, + .snowman, + .snowmanWithoutSnow, + .comet, + .fire, + .droplet, + .ocean, + ] + case .objects: + return [ + .eyeglasses, + .darkSunglasses, + .goggles, + .labCoat, + .safetyVest, + .necktie, + .shirt, + .jeans, + .scarf, + .gloves, + .coat, + .socks, + .dress, + .kimono, + .sari, + .onePieceSwimsuit, + .briefs, + .shorts, + .bikini, + .womansClothes, + .purse, + .handbag, + .pouch, + .shoppingBags, + .schoolSatchel, + .thongSandal, + .mansShoe, + .athleticShoe, + .hikingBoot, + .womansFlatShoe, + .highHeel, + .sandal, + .balletShoes, + .boot, + .crown, + .womansHat, + .tophat, + .mortarBoard, + .billedCap, + .militaryHelmet, + .helmetWithWhiteCross, + .prayerBeads, + .lipstick, + .ring, + .gem, + .mute, + .speaker, + .sound, + .loudSound, + .loudspeaker, + .mega, + .postalHorn, + .bell, + .noBell, + .musicalScore, + .musicalNote, + .notes, + .studioMicrophone, + .levelSlider, + .controlKnobs, + .microphone, + .headphones, + .radio, + .saxophone, + .accordion, + .guitar, + .musicalKeyboard, + .trumpet, + .violin, + .banjo, + .drumWithDrumsticks, + .longDrum, + .iphone, + .calling, + .phone, + .telephoneReceiver, + .pager, + .fax, + .battery, + .lowBattery, + .electricPlug, + .computer, + .desktopComputer, + .printer, + .keyboard, + .threeButtonMouse, + .trackball, + .minidisc, + .floppyDisk, + .cd, + .dvd, + .abacus, + .movieCamera, + .filmFrames, + .filmProjector, + .clapper, + .tv, + .camera, + .cameraWithFlash, + .videoCamera, + .vhs, + .mag, + .magRight, + .candle, + .bulb, + .flashlight, + .izakayaLantern, + .diyaLamp, + .notebookWithDecorativeCover, + .closedBook, + .book, + .greenBook, + .blueBook, + .orangeBook, + .books, + .notebook, + .ledger, + .pageWithCurl, + .scroll, + .pageFacingUp, + .newspaper, + .rolledUpNewspaper, + .bookmarkTabs, + .bookmark, + .label, + .moneybag, + .coin, + .yen, + .dollar, + .euro, + .pound, + .moneyWithWings, + .creditCard, + .receipt, + .chart, + .email, + .eMail, + .incomingEnvelope, + .envelopeWithArrow, + .outboxTray, + .inboxTray, + .package, + .mailbox, + .mailboxClosed, + .mailboxWithMail, + .mailboxWithNoMail, + .postbox, + .ballotBoxWithBallot, + .pencil2, + .blackNib, + .lowerLeftFountainPen, + .lowerLeftBallpointPen, + .lowerLeftPaintbrush, + .lowerLeftCrayon, + .memo, + .briefcase, + .fileFolder, + .openFileFolder, + .cardIndexDividers, + .date, + .calendar, + .spiralNotePad, + .spiralCalendarPad, + .cardIndex, + .chartWithUpwardsTrend, + .chartWithDownwardsTrend, + .barChart, + .clipboard, + .pushpin, + .roundPushpin, + .paperclip, + .linkedPaperclips, + .straightRuler, + .triangularRuler, + .scissors, + .cardFileBox, + .fileCabinet, + .wastebasket, + .lock, + .unlock, + .lockWithInkPen, + .closedLockWithKey, + .key, + .oldKey, + .hammer, + .axe, + .pick, + .hammerAndPick, + .hammerAndWrench, + .daggerKnife, + .crossedSwords, + .gun, + .boomerang, + .bowAndArrow, + .shield, + .carpentrySaw, + .wrench, + .screwdriver, + .nutAndBolt, + .gear, + .compression, + .scales, + .probingCane, + .link, + .chains, + .hook, + .toolbox, + .magnet, + .ladder, + .alembic, + .testTube, + .petriDish, + .dna, + .microscope, + .telescope, + .satelliteAntenna, + .syringe, + .dropOfBlood, + .pill, + .adhesiveBandage, + .crutch, + .stethoscope, + .xRay, + .door, + .elevator, + .mirror, + .window, + .bed, + .couchAndLamp, + .chair, + .toilet, + .plunger, + .shower, + .bathtub, + .mouseTrap, + .razor, + .lotionBottle, + .safetyPin, + .broom, + .basket, + .rollOfPaper, + .bucket, + .soap, + .bubbles, + .toothbrush, + .sponge, + .fireExtinguisher, + .shoppingTrolley, + .smoking, + .coffin, + .headstone, + .funeralUrn, + .moyai, + .placard, + .identificationCard, + ] + case .symbols: + return [ + .atm, + .putLitterInItsPlace, + .potableWater, + .wheelchair, + .mens, + .womens, + .restroom, + .babySymbol, + .wc, + .passportControl, + .customs, + .baggageClaim, + .leftLuggage, + .warning, + .childrenCrossing, + .noEntry, + .noEntrySign, + .noBicycles, + .noSmoking, + .doNotLitter, + .nonPotableWater, + .noPedestrians, + .noMobilePhones, + .underage, + .radioactiveSign, + .biohazardSign, + .arrowUp, + .arrowUpperRight, + .arrowRight, + .arrowLowerRight, + .arrowDown, + .arrowLowerLeft, + .arrowLeft, + .arrowUpperLeft, + .arrowUpDown, + .leftRightArrow, + .leftwardsArrowWithHook, + .arrowRightHook, + .arrowHeadingUp, + .arrowHeadingDown, + .arrowsClockwise, + .arrowsCounterclockwise, + .back, + .end, + .on, + .soon, + .top, + .placeOfWorship, + .atomSymbol, + .omSymbol, + .starOfDavid, + .wheelOfDharma, + .yinYang, + .latinCross, + .orthodoxCross, + .starAndCrescent, + .peaceSymbol, + .menorahWithNineBranches, + .sixPointedStar, + .aries, + .taurus, + .gemini, + .cancer, + .leo, + .virgo, + .libra, + .scorpius, + .sagittarius, + .capricorn, + .aquarius, + .pisces, + .ophiuchus, + .twistedRightwardsArrows, + .`repeat`, + .repeatOne, + .arrowForward, + .fastForward, + .blackRightPointingDoubleTriangleWithVerticalBar, + .blackRightPointingTriangleWithDoubleVerticalBar, + .arrowBackward, + .rewind, + .blackLeftPointingDoubleTriangleWithVerticalBar, + .arrowUpSmall, + .arrowDoubleUp, + .arrowDownSmall, + .arrowDoubleDown, + .doubleVerticalBar, + .blackSquareForStop, + .blackCircleForRecord, + .eject, + .cinema, + .lowBrightness, + .highBrightness, + .signalStrength, + .vibrationMode, + .mobilePhoneOff, + .femaleSign, + .maleSign, + .transgenderSymbol, + .heavyMultiplicationX, + .heavyPlusSign, + .heavyMinusSign, + .heavyDivisionSign, + .heavyEqualsSign, + .infinity, + .bangbang, + .interrobang, + .question, + .greyQuestion, + .greyExclamation, + .exclamation, + .wavyDash, + .currencyExchange, + .heavyDollarSign, + .medicalSymbol, + .recycle, + .fleurDeLis, + .trident, + .nameBadge, + .beginner, + .o, + .whiteCheckMark, + .ballotBoxWithCheck, + .heavyCheckMark, + .x, + .negativeSquaredCrossMark, + .curlyLoop, + .loop, + .partAlternationMark, + .eightSpokedAsterisk, + .eightPointedBlackStar, + .sparkle, + .copyright, + .registered, + .tm, + .hash, + .keycapStar, + .zero, + .one, + .two, + .three, + .four, + .five, + .six, + .seven, + .eight, + .nine, + .keycapTen, + .capitalAbcd, + .abcd, + .oneTwoThreeFour, + .symbols, + .abc, + .a, + .ab, + .b, + .cl, + .cool, + .free, + .informationSource, + .id, + .m, + .new, + .ng, + .o2, + .ok, + .parking, + .sos, + .up, + .vs, + .koko, + .sa, + .u6708, + .u6709, + .u6307, + .ideographAdvantage, + .u5272, + .u7121, + .u7981, + .accept, + .u7533, + .u5408, + .u7a7a, + .congratulations, + .secret, + .u55b6, + .u6e80, + .redCircle, + .largeOrangeCircle, + .largeYellowCircle, + .largeGreenCircle, + .largeBlueCircle, + .largePurpleCircle, + .largeBrownCircle, + .blackCircle, + .whiteCircle, + .largeRedSquare, + .largeOrangeSquare, + .largeYellowSquare, + .largeGreenSquare, + .largeBlueSquare, + .largePurpleSquare, + .largeBrownSquare, + .blackLargeSquare, + .whiteLargeSquare, + .blackMediumSquare, + .whiteMediumSquare, + .blackMediumSmallSquare, + .whiteMediumSmallSquare, + .blackSmallSquare, + .whiteSmallSquare, + .largeOrangeDiamond, + .largeBlueDiamond, + .smallOrangeDiamond, + .smallBlueDiamond, + .smallRedTriangle, + .smallRedTriangleDown, + .diamondShapeWithADotInside, + .radioButton, + .whiteSquareButton, + .blackSquareButton, + ] + case .flags: + return [ + .checkeredFlag, + .triangularFlagOnPost, + .crossedFlags, + .wavingBlackFlag, + .wavingWhiteFlag, + .rainbowFlag, + .transgenderFlag, + .pirateFlag, + .flagAc, + .flagAd, + .flagAe, + .flagAf, + .flagAg, + .flagAi, + .flagAl, + .flagAm, + .flagAo, + .flagAq, + .flagAr, + .flagAs, + .flagAt, + .flagAu, + .flagAw, + .flagAx, + .flagAz, + .flagBa, + .flagBb, + .flagBd, + .flagBe, + .flagBf, + .flagBg, + .flagBh, + .flagBi, + .flagBj, + .flagBl, + .flagBm, + .flagBn, + .flagBo, + .flagBq, + .flagBr, + .flagBs, + .flagBt, + .flagBv, + .flagBw, + .flagBy, + .flagBz, + .flagCa, + .flagCc, + .flagCd, + .flagCf, + .flagCg, + .flagCh, + .flagCi, + .flagCk, + .flagCl, + .flagCm, + .cn, + .flagCo, + .flagCp, + .flagCr, + .flagCu, + .flagCv, + .flagCw, + .flagCx, + .flagCy, + .flagCz, + .de, + .flagDg, + .flagDj, + .flagDk, + .flagDm, + .flagDo, + .flagDz, + .flagEa, + .flagEc, + .flagEe, + .flagEg, + .flagEh, + .flagEr, + .es, + .flagEt, + .flagEu, + .flagFi, + .flagFj, + .flagFk, + .flagFm, + .flagFo, + .fr, + .flagGa, + .gb, + .flagGd, + .flagGe, + .flagGf, + .flagGg, + .flagGh, + .flagGi, + .flagGl, + .flagGm, + .flagGn, + .flagGp, + .flagGq, + .flagGr, + .flagGs, + .flagGt, + .flagGu, + .flagGw, + .flagGy, + .flagHk, + .flagHm, + .flagHn, + .flagHr, + .flagHt, + .flagHu, + .flagIc, + .flagId, + .flagIe, + .flagIl, + .flagIm, + .flagIn, + .flagIo, + .flagIq, + .flagIr, + .flagIs, + .it, + .flagJe, + .flagJm, + .flagJo, + .jp, + .flagKe, + .flagKg, + .flagKh, + .flagKi, + .flagKm, + .flagKn, + .flagKp, + .kr, + .flagKw, + .flagKy, + .flagKz, + .flagLa, + .flagLb, + .flagLc, + .flagLi, + .flagLk, + .flagLr, + .flagLs, + .flagLt, + .flagLu, + .flagLv, + .flagLy, + .flagMa, + .flagMc, + .flagMd, + .flagMe, + .flagMf, + .flagMg, + .flagMh, + .flagMk, + .flagMl, + .flagMm, + .flagMn, + .flagMo, + .flagMp, + .flagMq, + .flagMr, + .flagMs, + .flagMt, + .flagMu, + .flagMv, + .flagMw, + .flagMx, + .flagMy, + .flagMz, + .flagNa, + .flagNc, + .flagNe, + .flagNf, + .flagNg, + .flagNi, + .flagNl, + .flagNo, + .flagNp, + .flagNr, + .flagNu, + .flagNz, + .flagOm, + .flagPa, + .flagPe, + .flagPf, + .flagPg, + .flagPh, + .flagPk, + .flagPl, + .flagPm, + .flagPn, + .flagPr, + .flagPs, + .flagPt, + .flagPw, + .flagPy, + .flagQa, + .flagRe, + .flagRo, + .flagRs, + .ru, + .flagRw, + .flagSa, + .flagSb, + .flagSc, + .flagSd, + .flagSe, + .flagSg, + .flagSh, + .flagSi, + .flagSj, + .flagSk, + .flagSl, + .flagSm, + .flagSn, + .flagSo, + .flagSr, + .flagSs, + .flagSt, + .flagSv, + .flagSx, + .flagSy, + .flagSz, + .flagTa, + .flagTc, + .flagTd, + .flagTf, + .flagTg, + .flagTh, + .flagTj, + .flagTk, + .flagTl, + .flagTm, + .flagTn, + .flagTo, + .flagTr, + .flagTt, + .flagTv, + .flagTw, + .flagTz, + .flagUa, + .flagUg, + .flagUn, + .us, + .flagUy, + .flagUz, + .flagVa, + .flagVc, + .flagVe, + .flagVg, + .flagVi, + .flagVn, + .flagVu, + .flagWf, + .flagWs, + .flagXk, + .flagYe, + .flagYt, + .flagZa, + .flagZm, + .flagZw, + .flagEngland, + .flagScotland, + .flagWales, + ] + } + } + } + + var category: Category { + switch self { + case .grinning: return .smileysAndPeople + case .smiley: return .smileysAndPeople + case .smile: return .smileysAndPeople + case .grin: return .smileysAndPeople + case .laughing: return .smileysAndPeople + case .sweatSmile: return .smileysAndPeople + case .rollingOnTheFloorLaughing: return .smileysAndPeople + case .joy: return .smileysAndPeople + case .slightlySmilingFace: return .smileysAndPeople + case .upsideDownFace: return .smileysAndPeople + case .meltingFace: return .smileysAndPeople + case .wink: return .smileysAndPeople + case .blush: return .smileysAndPeople + case .innocent: return .smileysAndPeople + case .smilingFaceWith3Hearts: return .smileysAndPeople + case .heartEyes: return .smileysAndPeople + case .starStruck: return .smileysAndPeople + case .kissingHeart: return .smileysAndPeople + case .kissing: return .smileysAndPeople + case .relaxed: return .smileysAndPeople + case .kissingClosedEyes: return .smileysAndPeople + case .kissingSmilingEyes: return .smileysAndPeople + case .smilingFaceWithTear: return .smileysAndPeople + case .yum: return .smileysAndPeople + case .stuckOutTongue: return .smileysAndPeople + case .stuckOutTongueWinkingEye: return .smileysAndPeople + case .zanyFace: return .smileysAndPeople + case .stuckOutTongueClosedEyes: return .smileysAndPeople + case .moneyMouthFace: return .smileysAndPeople + case .huggingFace: return .smileysAndPeople + case .faceWithHandOverMouth: return .smileysAndPeople + case .faceWithOpenEyesAndHandOverMouth: return .smileysAndPeople + case .faceWithPeekingEye: return .smileysAndPeople + case .shushingFace: return .smileysAndPeople + case .thinkingFace: return .smileysAndPeople + case .salutingFace: return .smileysAndPeople + case .zipperMouthFace: return .smileysAndPeople + case .faceWithRaisedEyebrow: return .smileysAndPeople + case .neutralFace: return .smileysAndPeople + case .expressionless: return .smileysAndPeople + case .noMouth: return .smileysAndPeople + case .dottedLineFace: return .smileysAndPeople + case .faceInClouds: return .smileysAndPeople + case .smirk: return .smileysAndPeople + case .unamused: return .smileysAndPeople + case .faceWithRollingEyes: return .smileysAndPeople + case .grimacing: return .smileysAndPeople + case .faceExhaling: return .smileysAndPeople + case .lyingFace: return .smileysAndPeople + case .relieved: return .smileysAndPeople + case .pensive: return .smileysAndPeople + case .sleepy: return .smileysAndPeople + case .droolingFace: return .smileysAndPeople + case .sleeping: return .smileysAndPeople + case .mask: return .smileysAndPeople + case .faceWithThermometer: return .smileysAndPeople + case .faceWithHeadBandage: return .smileysAndPeople + case .nauseatedFace: return .smileysAndPeople + case .faceVomiting: return .smileysAndPeople + case .sneezingFace: return .smileysAndPeople + case .hotFace: return .smileysAndPeople + case .coldFace: return .smileysAndPeople + case .woozyFace: return .smileysAndPeople + case .dizzyFace: return .smileysAndPeople + case .faceWithSpiralEyes: return .smileysAndPeople + case .explodingHead: return .smileysAndPeople + case .faceWithCowboyHat: return .smileysAndPeople + case .partyingFace: return .smileysAndPeople + case .disguisedFace: return .smileysAndPeople + case .sunglasses: return .smileysAndPeople + case .nerdFace: return .smileysAndPeople + case .faceWithMonocle: return .smileysAndPeople + case .confused: return .smileysAndPeople + case .faceWithDiagonalMouth: return .smileysAndPeople + case .worried: return .smileysAndPeople + case .slightlyFrowningFace: return .smileysAndPeople + case .whiteFrowningFace: return .smileysAndPeople + case .openMouth: return .smileysAndPeople + case .hushed: return .smileysAndPeople + case .astonished: return .smileysAndPeople + case .flushed: return .smileysAndPeople + case .pleadingFace: return .smileysAndPeople + case .faceHoldingBackTears: return .smileysAndPeople + case .frowning: return .smileysAndPeople + case .anguished: return .smileysAndPeople + case .fearful: return .smileysAndPeople + case .coldSweat: return .smileysAndPeople + case .disappointedRelieved: return .smileysAndPeople + case .cry: return .smileysAndPeople + case .sob: return .smileysAndPeople + case .scream: return .smileysAndPeople + case .confounded: return .smileysAndPeople + case .persevere: return .smileysAndPeople + case .disappointed: return .smileysAndPeople + case .sweat: return .smileysAndPeople + case .weary: return .smileysAndPeople + case .tiredFace: return .smileysAndPeople + case .yawningFace: return .smileysAndPeople + case .triumph: return .smileysAndPeople + case .rage: return .smileysAndPeople + case .angry: return .smileysAndPeople + case .faceWithSymbolsOnMouth: return .smileysAndPeople + case .smilingImp: return .smileysAndPeople + case .imp: return .smileysAndPeople + case .skull: return .smileysAndPeople + case .skullAndCrossbones: return .smileysAndPeople + case .hankey: return .smileysAndPeople + case .clownFace: return .smileysAndPeople + case .japaneseOgre: return .smileysAndPeople + case .japaneseGoblin: return .smileysAndPeople + case .ghost: return .smileysAndPeople + case .alien: return .smileysAndPeople + case .spaceInvader: return .smileysAndPeople + case .robotFace: return .smileysAndPeople + case .smileyCat: return .smileysAndPeople + case .smileCat: return .smileysAndPeople + case .joyCat: return .smileysAndPeople + case .heartEyesCat: return .smileysAndPeople + case .smirkCat: return .smileysAndPeople + case .kissingCat: return .smileysAndPeople + case .screamCat: return .smileysAndPeople + case .cryingCatFace: return .smileysAndPeople + case .poutingCat: return .smileysAndPeople + case .seeNoEvil: return .smileysAndPeople + case .hearNoEvil: return .smileysAndPeople + case .speakNoEvil: return .smileysAndPeople + case .kiss: return .smileysAndPeople + case .loveLetter: return .smileysAndPeople + case .cupid: return .smileysAndPeople + case .giftHeart: return .smileysAndPeople + case .sparklingHeart: return .smileysAndPeople + case .heartpulse: return .smileysAndPeople + case .heartbeat: return .smileysAndPeople + case .revolvingHearts: return .smileysAndPeople + case .twoHearts: return .smileysAndPeople + case .heartDecoration: return .smileysAndPeople + case .heavyHeartExclamationMarkOrnament: return .smileysAndPeople + case .brokenHeart: return .smileysAndPeople + case .heartOnFire: return .smileysAndPeople + case .mendingHeart: return .smileysAndPeople + case .heart: return .smileysAndPeople + case .orangeHeart: return .smileysAndPeople + case .yellowHeart: return .smileysAndPeople + case .greenHeart: return .smileysAndPeople + case .blueHeart: return .smileysAndPeople + case .purpleHeart: return .smileysAndPeople + case .brownHeart: return .smileysAndPeople + case .blackHeart: return .smileysAndPeople + case .whiteHeart: return .smileysAndPeople + case .oneHundred: return .smileysAndPeople + case .anger: return .smileysAndPeople + case .boom: return .smileysAndPeople + case .dizzy: return .smileysAndPeople + case .sweatDrops: return .smileysAndPeople + case .dash: return .smileysAndPeople + case .hole: return .smileysAndPeople + case .bomb: return .smileysAndPeople + case .speechBalloon: return .smileysAndPeople + case .eyeInSpeechBubble: return .smileysAndPeople + case .leftSpeechBubble: return .smileysAndPeople + case .rightAngerBubble: return .smileysAndPeople + case .thoughtBalloon: return .smileysAndPeople + case .zzz: return .smileysAndPeople + case .wave: return .smileysAndPeople + case .raisedBackOfHand: return .smileysAndPeople + case .raisedHandWithFingersSplayed: return .smileysAndPeople + case .hand: return .smileysAndPeople + case .spockHand: return .smileysAndPeople + case .rightwardsHand: return .smileysAndPeople + case .leftwardsHand: return .smileysAndPeople + case .palmDownHand: return .smileysAndPeople + case .palmUpHand: return .smileysAndPeople + case .okHand: return .smileysAndPeople + case .pinchedFingers: return .smileysAndPeople + case .pinchingHand: return .smileysAndPeople + case .v: return .smileysAndPeople + case .crossedFingers: return .smileysAndPeople + case .handWithIndexFingerAndThumbCrossed: return .smileysAndPeople + case .iLoveYouHandSign: return .smileysAndPeople + case .theHorns: return .smileysAndPeople + case .callMeHand: return .smileysAndPeople + case .pointLeft: return .smileysAndPeople + case .pointRight: return .smileysAndPeople + case .pointUp2: return .smileysAndPeople + case .middleFinger: return .smileysAndPeople + case .pointDown: return .smileysAndPeople + case .pointUp: return .smileysAndPeople + case .indexPointingAtTheViewer: return .smileysAndPeople + case .plusOne: return .smileysAndPeople + case .negativeOne: return .smileysAndPeople + case .fist: return .smileysAndPeople + case .facepunch: return .smileysAndPeople + case .leftFacingFist: return .smileysAndPeople + case .rightFacingFist: return .smileysAndPeople + case .clap: return .smileysAndPeople + case .raisedHands: return .smileysAndPeople + case .heartHands: return .smileysAndPeople + case .openHands: return .smileysAndPeople + case .palmsUpTogether: return .smileysAndPeople + case .handshake: return .smileysAndPeople + case .pray: return .smileysAndPeople + case .writingHand: return .smileysAndPeople + case .nailCare: return .smileysAndPeople + case .selfie: return .smileysAndPeople + case .muscle: return .smileysAndPeople + case .mechanicalArm: return .smileysAndPeople + case .mechanicalLeg: return .smileysAndPeople + case .leg: return .smileysAndPeople + case .foot: return .smileysAndPeople + case .ear: return .smileysAndPeople + case .earWithHearingAid: return .smileysAndPeople + case .nose: return .smileysAndPeople + case .brain: return .smileysAndPeople + case .anatomicalHeart: return .smileysAndPeople + case .lungs: return .smileysAndPeople + case .tooth: return .smileysAndPeople + case .bone: return .smileysAndPeople + case .eyes: return .smileysAndPeople + case .eye: return .smileysAndPeople + case .tongue: return .smileysAndPeople + case .lips: return .smileysAndPeople + case .bitingLip: return .smileysAndPeople + case .baby: return .smileysAndPeople + case .child: return .smileysAndPeople + case .boy: return .smileysAndPeople + case .girl: return .smileysAndPeople + case .adult: return .smileysAndPeople + case .personWithBlondHair: return .smileysAndPeople + case .man: return .smileysAndPeople + case .beardedPerson: return .smileysAndPeople + case .manWithBeard: return .smileysAndPeople + case .womanWithBeard: return .smileysAndPeople + case .redHairedMan: return .smileysAndPeople + case .curlyHairedMan: return .smileysAndPeople + case .whiteHairedMan: return .smileysAndPeople + case .baldMan: return .smileysAndPeople + case .woman: return .smileysAndPeople + case .redHairedWoman: return .smileysAndPeople + case .redHairedPerson: return .smileysAndPeople + case .curlyHairedWoman: return .smileysAndPeople + case .curlyHairedPerson: return .smileysAndPeople + case .whiteHairedWoman: return .smileysAndPeople + case .whiteHairedPerson: return .smileysAndPeople + case .baldWoman: return .smileysAndPeople + case .baldPerson: return .smileysAndPeople + case .blondHairedWoman: return .smileysAndPeople + case .blondHairedMan: return .smileysAndPeople + case .olderAdult: return .smileysAndPeople + case .olderMan: return .smileysAndPeople + case .olderWoman: return .smileysAndPeople + case .personFrowning: return .smileysAndPeople + case .manFrowning: return .smileysAndPeople + case .womanFrowning: return .smileysAndPeople + case .personWithPoutingFace: return .smileysAndPeople + case .manPouting: return .smileysAndPeople + case .womanPouting: return .smileysAndPeople + case .noGood: return .smileysAndPeople + case .manGesturingNo: return .smileysAndPeople + case .womanGesturingNo: return .smileysAndPeople + case .okWoman: return .smileysAndPeople + case .manGesturingOk: return .smileysAndPeople + case .womanGesturingOk: return .smileysAndPeople + case .informationDeskPerson: return .smileysAndPeople + case .manTippingHand: return .smileysAndPeople + case .womanTippingHand: return .smileysAndPeople + case .raisingHand: return .smileysAndPeople + case .manRaisingHand: return .smileysAndPeople + case .womanRaisingHand: return .smileysAndPeople + case .deafPerson: return .smileysAndPeople + case .deafMan: return .smileysAndPeople + case .deafWoman: return .smileysAndPeople + case .bow: return .smileysAndPeople + case .manBowing: return .smileysAndPeople + case .womanBowing: return .smileysAndPeople + case .facePalm: return .smileysAndPeople + case .manFacepalming: return .smileysAndPeople + case .womanFacepalming: return .smileysAndPeople + case .shrug: return .smileysAndPeople + case .manShrugging: return .smileysAndPeople + case .womanShrugging: return .smileysAndPeople + case .healthWorker: return .smileysAndPeople + case .maleDoctor: return .smileysAndPeople + case .femaleDoctor: return .smileysAndPeople + case .student: return .smileysAndPeople + case .maleStudent: return .smileysAndPeople + case .femaleStudent: return .smileysAndPeople + case .teacher: return .smileysAndPeople + case .maleTeacher: return .smileysAndPeople + case .femaleTeacher: return .smileysAndPeople + case .judge: return .smileysAndPeople + case .maleJudge: return .smileysAndPeople + case .femaleJudge: return .smileysAndPeople + case .farmer: return .smileysAndPeople + case .maleFarmer: return .smileysAndPeople + case .femaleFarmer: return .smileysAndPeople + case .cook: return .smileysAndPeople + case .maleCook: return .smileysAndPeople + case .femaleCook: return .smileysAndPeople + case .mechanic: return .smileysAndPeople + case .maleMechanic: return .smileysAndPeople + case .femaleMechanic: return .smileysAndPeople + case .factoryWorker: return .smileysAndPeople + case .maleFactoryWorker: return .smileysAndPeople + case .femaleFactoryWorker: return .smileysAndPeople + case .officeWorker: return .smileysAndPeople + case .maleOfficeWorker: return .smileysAndPeople + case .femaleOfficeWorker: return .smileysAndPeople + case .scientist: return .smileysAndPeople + case .maleScientist: return .smileysAndPeople + case .femaleScientist: return .smileysAndPeople + case .technologist: return .smileysAndPeople + case .maleTechnologist: return .smileysAndPeople + case .femaleTechnologist: return .smileysAndPeople + case .singer: return .smileysAndPeople + case .maleSinger: return .smileysAndPeople + case .femaleSinger: return .smileysAndPeople + case .artist: return .smileysAndPeople + case .maleArtist: return .smileysAndPeople + case .femaleArtist: return .smileysAndPeople + case .pilot: return .smileysAndPeople + case .malePilot: return .smileysAndPeople + case .femalePilot: return .smileysAndPeople + case .astronaut: return .smileysAndPeople + case .maleAstronaut: return .smileysAndPeople + case .femaleAstronaut: return .smileysAndPeople + case .firefighter: return .smileysAndPeople + case .maleFirefighter: return .smileysAndPeople + case .femaleFirefighter: return .smileysAndPeople + case .cop: return .smileysAndPeople + case .malePoliceOfficer: return .smileysAndPeople + case .femalePoliceOfficer: return .smileysAndPeople + case .sleuthOrSpy: return .smileysAndPeople + case .maleDetective: return .smileysAndPeople + case .femaleDetective: return .smileysAndPeople + case .guardsman: return .smileysAndPeople + case .maleGuard: return .smileysAndPeople + case .femaleGuard: return .smileysAndPeople + case .ninja: return .smileysAndPeople + case .constructionWorker: return .smileysAndPeople + case .maleConstructionWorker: return .smileysAndPeople + case .femaleConstructionWorker: return .smileysAndPeople + case .personWithCrown: return .smileysAndPeople + case .prince: return .smileysAndPeople + case .princess: return .smileysAndPeople + case .manWithTurban: return .smileysAndPeople + case .manWearingTurban: return .smileysAndPeople + case .womanWearingTurban: return .smileysAndPeople + case .manWithGuaPiMao: return .smileysAndPeople + case .personWithHeadscarf: return .smileysAndPeople + case .personInTuxedo: return .smileysAndPeople + case .manInTuxedo: return .smileysAndPeople + case .womanInTuxedo: return .smileysAndPeople + case .brideWithVeil: return .smileysAndPeople + case .manWithVeil: return .smileysAndPeople + case .womanWithVeil: return .smileysAndPeople + case .pregnantWoman: return .smileysAndPeople + case .pregnantMan: return .smileysAndPeople + case .pregnantPerson: return .smileysAndPeople + case .breastFeeding: return .smileysAndPeople + case .womanFeedingBaby: return .smileysAndPeople + case .manFeedingBaby: return .smileysAndPeople + case .personFeedingBaby: return .smileysAndPeople + case .angel: return .smileysAndPeople + case .santa: return .smileysAndPeople + case .mrsClaus: return .smileysAndPeople + case .mxClaus: return .smileysAndPeople + case .superhero: return .smileysAndPeople + case .maleSuperhero: return .smileysAndPeople + case .femaleSuperhero: return .smileysAndPeople + case .supervillain: return .smileysAndPeople + case .maleSupervillain: return .smileysAndPeople + case .femaleSupervillain: return .smileysAndPeople + case .mage: return .smileysAndPeople + case .maleMage: return .smileysAndPeople + case .femaleMage: return .smileysAndPeople + case .fairy: return .smileysAndPeople + case .maleFairy: return .smileysAndPeople + case .femaleFairy: return .smileysAndPeople + case .vampire: return .smileysAndPeople + case .maleVampire: return .smileysAndPeople + case .femaleVampire: return .smileysAndPeople + case .merperson: return .smileysAndPeople + case .merman: return .smileysAndPeople + case .mermaid: return .smileysAndPeople + case .elf: return .smileysAndPeople + case .maleElf: return .smileysAndPeople + case .femaleElf: return .smileysAndPeople + case .genie: return .smileysAndPeople + case .maleGenie: return .smileysAndPeople + case .femaleGenie: return .smileysAndPeople + case .zombie: return .smileysAndPeople + case .maleZombie: return .smileysAndPeople + case .femaleZombie: return .smileysAndPeople + case .troll: return .smileysAndPeople + case .massage: return .smileysAndPeople + case .manGettingMassage: return .smileysAndPeople + case .womanGettingMassage: return .smileysAndPeople + case .haircut: return .smileysAndPeople + case .manGettingHaircut: return .smileysAndPeople + case .womanGettingHaircut: return .smileysAndPeople + case .walking: return .smileysAndPeople + case .manWalking: return .smileysAndPeople + case .womanWalking: return .smileysAndPeople + case .standingPerson: return .smileysAndPeople + case .manStanding: return .smileysAndPeople + case .womanStanding: return .smileysAndPeople + case .kneelingPerson: return .smileysAndPeople + case .manKneeling: return .smileysAndPeople + case .womanKneeling: return .smileysAndPeople + case .personWithProbingCane: return .smileysAndPeople + case .manWithProbingCane: return .smileysAndPeople + case .womanWithProbingCane: return .smileysAndPeople + case .personInMotorizedWheelchair: return .smileysAndPeople + case .manInMotorizedWheelchair: return .smileysAndPeople + case .womanInMotorizedWheelchair: return .smileysAndPeople + case .personInManualWheelchair: return .smileysAndPeople + case .manInManualWheelchair: return .smileysAndPeople + case .womanInManualWheelchair: return .smileysAndPeople + case .runner: return .smileysAndPeople + case .manRunning: return .smileysAndPeople + case .womanRunning: return .smileysAndPeople + case .dancer: return .smileysAndPeople + case .manDancing: return .smileysAndPeople + case .manInBusinessSuitLevitating: return .smileysAndPeople + case .dancers: return .smileysAndPeople + case .menWithBunnyEarsPartying: return .smileysAndPeople + case .womenWithBunnyEarsPartying: return .smileysAndPeople + case .personInSteamyRoom: return .smileysAndPeople + case .manInSteamyRoom: return .smileysAndPeople + case .womanInSteamyRoom: return .smileysAndPeople + case .personClimbing: return .smileysAndPeople + case .manClimbing: return .smileysAndPeople + case .womanClimbing: return .smileysAndPeople + case .fencer: return .smileysAndPeople + case .horseRacing: return .smileysAndPeople + case .skier: return .smileysAndPeople + case .snowboarder: return .smileysAndPeople + case .golfer: return .smileysAndPeople + case .manGolfing: return .smileysAndPeople + case .womanGolfing: return .smileysAndPeople + case .surfer: return .smileysAndPeople + case .manSurfing: return .smileysAndPeople + case .womanSurfing: return .smileysAndPeople + case .rowboat: return .smileysAndPeople + case .manRowingBoat: return .smileysAndPeople + case .womanRowingBoat: return .smileysAndPeople + case .swimmer: return .smileysAndPeople + case .manSwimming: return .smileysAndPeople + case .womanSwimming: return .smileysAndPeople + case .personWithBall: return .smileysAndPeople + case .manBouncingBall: return .smileysAndPeople + case .womanBouncingBall: return .smileysAndPeople + case .weightLifter: return .smileysAndPeople + case .manLiftingWeights: return .smileysAndPeople + case .womanLiftingWeights: return .smileysAndPeople + case .bicyclist: return .smileysAndPeople + case .manBiking: return .smileysAndPeople + case .womanBiking: return .smileysAndPeople + case .mountainBicyclist: return .smileysAndPeople + case .manMountainBiking: return .smileysAndPeople + case .womanMountainBiking: return .smileysAndPeople + case .personDoingCartwheel: return .smileysAndPeople + case .manCartwheeling: return .smileysAndPeople + case .womanCartwheeling: return .smileysAndPeople + case .wrestlers: return .smileysAndPeople + case .manWrestling: return .smileysAndPeople + case .womanWrestling: return .smileysAndPeople + case .waterPolo: return .smileysAndPeople + case .manPlayingWaterPolo: return .smileysAndPeople + case .womanPlayingWaterPolo: return .smileysAndPeople + case .handball: return .smileysAndPeople + case .manPlayingHandball: return .smileysAndPeople + case .womanPlayingHandball: return .smileysAndPeople + case .juggling: return .smileysAndPeople + case .manJuggling: return .smileysAndPeople + case .womanJuggling: return .smileysAndPeople + case .personInLotusPosition: return .smileysAndPeople + case .manInLotusPosition: return .smileysAndPeople + case .womanInLotusPosition: return .smileysAndPeople + case .bath: return .smileysAndPeople + case .sleepingAccommodation: return .smileysAndPeople + case .peopleHoldingHands: return .smileysAndPeople + case .twoWomenHoldingHands: return .smileysAndPeople + case .manAndWomanHoldingHands: return .smileysAndPeople + case .twoMenHoldingHands: return .smileysAndPeople + case .personKissPerson: return .smileysAndPeople + case .womanKissMan: return .smileysAndPeople + case .manKissMan: return .smileysAndPeople + case .womanKissWoman: return .smileysAndPeople + case .personHeartPerson: return .smileysAndPeople + case .womanHeartMan: return .smileysAndPeople + case .manHeartMan: return .smileysAndPeople + case .womanHeartWoman: return .smileysAndPeople + case .family: return .smileysAndPeople + case .manWomanBoy: return .smileysAndPeople + case .manWomanGirl: return .smileysAndPeople + case .manWomanGirlBoy: return .smileysAndPeople + case .manWomanBoyBoy: return .smileysAndPeople + case .manWomanGirlGirl: return .smileysAndPeople + case .manManBoy: return .smileysAndPeople + case .manManGirl: return .smileysAndPeople + case .manManGirlBoy: return .smileysAndPeople + case .manManBoyBoy: return .smileysAndPeople + case .manManGirlGirl: return .smileysAndPeople + case .womanWomanBoy: return .smileysAndPeople + case .womanWomanGirl: return .smileysAndPeople + case .womanWomanGirlBoy: return .smileysAndPeople + case .womanWomanBoyBoy: return .smileysAndPeople + case .womanWomanGirlGirl: return .smileysAndPeople + case .manBoy: return .smileysAndPeople + case .manBoyBoy: return .smileysAndPeople + case .manGirl: return .smileysAndPeople + case .manGirlBoy: return .smileysAndPeople + case .manGirlGirl: return .smileysAndPeople + case .womanBoy: return .smileysAndPeople + case .womanBoyBoy: return .smileysAndPeople + case .womanGirl: return .smileysAndPeople + case .womanGirlBoy: return .smileysAndPeople + case .womanGirlGirl: return .smileysAndPeople + case .speakingHeadInSilhouette: return .smileysAndPeople + case .bustInSilhouette: return .smileysAndPeople + case .bustsInSilhouette: return .smileysAndPeople + case .peopleHugging: return .smileysAndPeople + case .footprints: return .smileysAndPeople + case .monkeyFace: return .animals + case .monkey: return .animals + case .gorilla: return .animals + case .orangutan: return .animals + case .dog: return .animals + case .dog2: return .animals + case .guideDog: return .animals + case .serviceDog: return .animals + case .poodle: return .animals + case .wolf: return .animals + case .foxFace: return .animals + case .raccoon: return .animals + case .cat: return .animals + case .cat2: return .animals + case .blackCat: return .animals + case .lionFace: return .animals + case .tiger: return .animals + case .tiger2: return .animals + case .leopard: return .animals + case .horse: return .animals + case .racehorse: return .animals + case .unicornFace: return .animals + case .zebraFace: return .animals + case .deer: return .animals + case .bison: return .animals + case .cow: return .animals + case .ox: return .animals + case .waterBuffalo: return .animals + case .cow2: return .animals + case .pig: return .animals + case .pig2: return .animals + case .boar: return .animals + case .pigNose: return .animals + case .ram: return .animals + case .sheep: return .animals + case .goat: return .animals + case .dromedaryCamel: return .animals + case .camel: return .animals + case .llama: return .animals + case .giraffeFace: return .animals + case .elephant: return .animals + case .mammoth: return .animals + case .rhinoceros: return .animals + case .hippopotamus: return .animals + case .mouse: return .animals + case .mouse2: return .animals + case .rat: return .animals + case .hamster: return .animals + case .rabbit: return .animals + case .rabbit2: return .animals + case .chipmunk: return .animals + case .beaver: return .animals + case .hedgehog: return .animals + case .bat: return .animals + case .bear: return .animals + case .polarBear: return .animals + case .koala: return .animals + case .pandaFace: return .animals + case .sloth: return .animals + case .otter: return .animals + case .skunk: return .animals + case .kangaroo: return .animals + case .badger: return .animals + case .feet: return .animals + case .turkey: return .animals + case .chicken: return .animals + case .rooster: return .animals + case .hatchingChick: return .animals + case .babyChick: return .animals + case .hatchedChick: return .animals + case .bird: return .animals + case .penguin: return .animals + case .doveOfPeace: return .animals + case .eagle: return .animals + case .duck: return .animals + case .swan: return .animals + case .owl: return .animals + case .dodo: return .animals + case .feather: return .animals + case .flamingo: return .animals + case .peacock: return .animals + case .parrot: return .animals + case .frog: return .animals + case .crocodile: return .animals + case .turtle: return .animals + case .lizard: return .animals + case .snake: return .animals + case .dragonFace: return .animals + case .dragon: return .animals + case .sauropod: return .animals + case .tRex: return .animals + case .whale: return .animals + case .whale2: return .animals + case .dolphin: return .animals + case .seal: return .animals + case .fish: return .animals + case .tropicalFish: return .animals + case .blowfish: return .animals + case .shark: return .animals + case .octopus: return .animals + case .shell: return .animals + case .coral: return .animals + case .snail: return .animals + case .butterfly: return .animals + case .bug: return .animals + case .ant: return .animals + case .bee: return .animals + case .beetle: return .animals + case .ladybug: return .animals + case .cricket: return .animals + case .cockroach: return .animals + case .spider: return .animals + case .spiderWeb: return .animals + case .scorpion: return .animals + case .mosquito: return .animals + case .fly: return .animals + case .worm: return .animals + case .microbe: return .animals + case .bouquet: return .animals + case .cherryBlossom: return .animals + case .whiteFlower: return .animals + case .lotus: return .animals + case .rosette: return .animals + case .rose: return .animals + case .wiltedFlower: return .animals + case .hibiscus: return .animals + case .sunflower: return .animals + case .blossom: return .animals + case .tulip: return .animals + case .seedling: return .animals + case .pottedPlant: return .animals + case .evergreenTree: return .animals + case .deciduousTree: return .animals + case .palmTree: return .animals + case .cactus: return .animals + case .earOfRice: return .animals + case .herb: return .animals + case .shamrock: return .animals + case .fourLeafClover: return .animals + case .mapleLeaf: return .animals + case .fallenLeaf: return .animals + case .leaves: return .animals + case .emptyNest: return .animals + case .nestWithEggs: return .animals + case .grapes: return .food + case .melon: return .food + case .watermelon: return .food + case .tangerine: return .food + case .lemon: return .food + case .banana: return .food + case .pineapple: return .food + case .mango: return .food + case .apple: return .food + case .greenApple: return .food + case .pear: return .food + case .peach: return .food + case .cherries: return .food + case .strawberry: return .food + case .blueberries: return .food + case .kiwifruit: return .food + case .tomato: return .food + case .olive: return .food + case .coconut: return .food + case .avocado: return .food + case .eggplant: return .food + case .potato: return .food + case .carrot: return .food + case .corn: return .food + case .hotPepper: return .food + case .bellPepper: return .food + case .cucumber: return .food + case .leafyGreen: return .food + case .broccoli: return .food + case .garlic: return .food + case .onion: return .food + case .mushroom: return .food + case .peanuts: return .food + case .beans: return .food + case .chestnut: return .food + case .bread: return .food + case .croissant: return .food + case .baguetteBread: return .food + case .flatbread: return .food + case .pretzel: return .food + case .bagel: return .food + case .pancakes: return .food + case .waffle: return .food + case .cheeseWedge: return .food + case .meatOnBone: return .food + case .poultryLeg: return .food + case .cutOfMeat: return .food + case .bacon: return .food + case .hamburger: return .food + case .fries: return .food + case .pizza: return .food + case .hotdog: return .food + case .sandwich: return .food + case .taco: return .food + case .burrito: return .food + case .tamale: return .food + case .stuffedFlatbread: return .food + case .falafel: return .food + case .egg: return .food + case .friedEgg: return .food + case .shallowPanOfFood: return .food + case .stew: return .food + case .fondue: return .food + case .bowlWithSpoon: return .food + case .greenSalad: return .food + case .popcorn: return .food + case .butter: return .food + case .salt: return .food + case .cannedFood: return .food + case .bento: return .food + case .riceCracker: return .food + case .riceBall: return .food + case .rice: return .food + case .curry: return .food + case .ramen: return .food + case .spaghetti: return .food + case .sweetPotato: return .food + case .oden: return .food + case .sushi: return .food + case .friedShrimp: return .food + case .fishCake: return .food + case .moonCake: return .food + case .dango: return .food + case .dumpling: return .food + case .fortuneCookie: return .food + case .takeoutBox: return .food + case .crab: return .food + case .lobster: return .food + case .shrimp: return .food + case .squid: return .food + case .oyster: return .food + case .icecream: return .food + case .shavedIce: return .food + case .iceCream: return .food + case .doughnut: return .food + case .cookie: return .food + case .birthday: return .food + case .cake: return .food + case .cupcake: return .food + case .pie: return .food + case .chocolateBar: return .food + case .candy: return .food + case .lollipop: return .food + case .custard: return .food + case .honeyPot: return .food + case .babyBottle: return .food + case .glassOfMilk: return .food + case .coffee: return .food + case .teapot: return .food + case .tea: return .food + case .sake: return .food + case .champagne: return .food + case .wineGlass: return .food + case .cocktail: return .food + case .tropicalDrink: return .food + case .beer: return .food + case .beers: return .food + case .clinkingGlasses: return .food + case .tumblerGlass: return .food + case .pouringLiquid: return .food + case .cupWithStraw: return .food + case .bubbleTea: return .food + case .beverageBox: return .food + case .mateDrink: return .food + case .iceCube: return .food + case .chopsticks: return .food + case .knifeForkPlate: return .food + case .forkAndKnife: return .food + case .spoon: return .food + case .hocho: return .food + case .jar: return .food + case .amphora: return .food + case .earthAfrica: return .travel + case .earthAmericas: return .travel + case .earthAsia: return .travel + case .globeWithMeridians: return .travel + case .worldMap: return .travel + case .japan: return .travel + case .compass: return .travel + case .snowCappedMountain: return .travel + case .mountain: return .travel + case .volcano: return .travel + case .mountFuji: return .travel + case .camping: return .travel + case .beachWithUmbrella: return .travel + case .desert: return .travel + case .desertIsland: return .travel + case .nationalPark: return .travel + case .stadium: return .travel + case .classicalBuilding: return .travel + case .buildingConstruction: return .travel + case .bricks: return .travel + case .rock: return .travel + case .wood: return .travel + case .hut: return .travel + case .houseBuildings: return .travel + case .derelictHouseBuilding: return .travel + case .house: return .travel + case .houseWithGarden: return .travel + case .office: return .travel + case .postOffice: return .travel + case .europeanPostOffice: return .travel + case .hospital: return .travel + case .bank: return .travel + case .hotel: return .travel + case .loveHotel: return .travel + case .convenienceStore: return .travel + case .school: return .travel + case .departmentStore: return .travel + case .factory: return .travel + case .japaneseCastle: return .travel + case .europeanCastle: return .travel + case .wedding: return .travel + case .tokyoTower: return .travel + case .statueOfLiberty: return .travel + case .church: return .travel + case .mosque: return .travel + case .hinduTemple: return .travel + case .synagogue: return .travel + case .shintoShrine: return .travel + case .kaaba: return .travel + case .fountain: return .travel + case .tent: return .travel + case .foggy: return .travel + case .nightWithStars: return .travel + case .cityscape: return .travel + case .sunriseOverMountains: return .travel + case .sunrise: return .travel + case .citySunset: return .travel + case .citySunrise: return .travel + case .bridgeAtNight: return .travel + case .hotsprings: return .travel + case .carouselHorse: return .travel + case .playgroundSlide: return .travel + case .ferrisWheel: return .travel + case .rollerCoaster: return .travel + case .barber: return .travel + case .circusTent: return .travel + case .steamLocomotive: return .travel + case .railwayCar: return .travel + case .bullettrainSide: return .travel + case .bullettrainFront: return .travel + case .train2: return .travel + case .metro: return .travel + case .lightRail: return .travel + case .station: return .travel + case .tram: return .travel + case .monorail: return .travel + case .mountainRailway: return .travel + case .train: return .travel + case .bus: return .travel + case .oncomingBus: return .travel + case .trolleybus: return .travel + case .minibus: return .travel + case .ambulance: return .travel + case .fireEngine: return .travel + case .policeCar: return .travel + case .oncomingPoliceCar: return .travel + case .taxi: return .travel + case .oncomingTaxi: return .travel + case .car: return .travel + case .oncomingAutomobile: return .travel + case .blueCar: return .travel + case .pickupTruck: return .travel + case .truck: return .travel + case .articulatedLorry: return .travel + case .tractor: return .travel + case .racingCar: return .travel + case .racingMotorcycle: return .travel + case .motorScooter: return .travel + case .manualWheelchair: return .travel + case .motorizedWheelchair: return .travel + case .autoRickshaw: return .travel + case .bike: return .travel + case .scooter: return .travel + case .skateboard: return .travel + case .rollerSkate: return .travel + case .busstop: return .travel + case .motorway: return .travel + case .railwayTrack: return .travel + case .oilDrum: return .travel + case .fuelpump: return .travel + case .wheel: return .travel + case .rotatingLight: return .travel + case .trafficLight: return .travel + case .verticalTrafficLight: return .travel + case .octagonalSign: return .travel + case .construction: return .travel + case .anchor: return .travel + case .ringBuoy: return .travel + case .boat: return .travel + case .canoe: return .travel + case .speedboat: return .travel + case .passengerShip: return .travel + case .ferry: return .travel + case .motorBoat: return .travel + case .ship: return .travel + case .airplane: return .travel + case .smallAirplane: return .travel + case .airplaneDeparture: return .travel + case .airplaneArriving: return .travel + case .parachute: return .travel + case .seat: return .travel + case .helicopter: return .travel + case .suspensionRailway: return .travel + case .mountainCableway: return .travel + case .aerialTramway: return .travel + case .satellite: return .travel + case .rocket: return .travel + case .flyingSaucer: return .travel + case .bellhopBell: return .travel + case .luggage: return .travel + case .hourglass: return .travel + case .hourglassFlowingSand: return .travel + case .watch: return .travel + case .alarmClock: return .travel + case .stopwatch: return .travel + case .timerClock: return .travel + case .mantelpieceClock: return .travel + case .clock12: return .travel + case .clock1230: return .travel + case .clock1: return .travel + case .clock130: return .travel + case .clock2: return .travel + case .clock230: return .travel + case .clock3: return .travel + case .clock330: return .travel + case .clock4: return .travel + case .clock430: return .travel + case .clock5: return .travel + case .clock530: return .travel + case .clock6: return .travel + case .clock630: return .travel + case .clock7: return .travel + case .clock730: return .travel + case .clock8: return .travel + case .clock830: return .travel + case .clock9: return .travel + case .clock930: return .travel + case .clock10: return .travel + case .clock1030: return .travel + case .clock11: return .travel + case .clock1130: return .travel + case .newMoon: return .travel + case .waxingCrescentMoon: return .travel + case .firstQuarterMoon: return .travel + case .moon: return .travel + case .fullMoon: return .travel + case .waningGibbousMoon: return .travel + case .lastQuarterMoon: return .travel + case .waningCrescentMoon: return .travel + case .crescentMoon: return .travel + case .newMoonWithFace: return .travel + case .firstQuarterMoonWithFace: return .travel + case .lastQuarterMoonWithFace: return .travel + case .thermometer: return .travel + case .sunny: return .travel + case .fullMoonWithFace: return .travel + case .sunWithFace: return .travel + case .ringedPlanet: return .travel + case .star: return .travel + case .star2: return .travel + case .stars: return .travel + case .milkyWay: return .travel + case .cloud: return .travel + case .partlySunny: return .travel + case .thunderCloudAndRain: return .travel + case .mostlySunny: return .travel + case .barelySunny: return .travel + case .partlySunnyRain: return .travel + case .rainCloud: return .travel + case .snowCloud: return .travel + case .lightning: return .travel + case .tornado: return .travel + case .fog: return .travel + case .windBlowingFace: return .travel + case .cyclone: return .travel + case .rainbow: return .travel + case .closedUmbrella: return .travel + case .umbrella: return .travel + case .umbrellaWithRainDrops: return .travel + case .umbrellaOnGround: return .travel + case .zap: return .travel + case .snowflake: return .travel + case .snowman: return .travel + case .snowmanWithoutSnow: return .travel + case .comet: return .travel + case .fire: return .travel + case .droplet: return .travel + case .ocean: return .travel + case .jackOLantern: return .activities + case .christmasTree: return .activities + case .fireworks: return .activities + case .sparkler: return .activities + case .firecracker: return .activities + case .sparkles: return .activities + case .balloon: return .activities + case .tada: return .activities + case .confettiBall: return .activities + case .tanabataTree: return .activities + case .bamboo: return .activities + case .dolls: return .activities + case .flags: return .activities + case .windChime: return .activities + case .riceScene: return .activities + case .redEnvelope: return .activities + case .ribbon: return .activities + case .gift: return .activities + case .reminderRibbon: return .activities + case .admissionTickets: return .activities + case .ticket: return .activities + case .medal: return .activities + case .trophy: return .activities + case .sportsMedal: return .activities + case .firstPlaceMedal: return .activities + case .secondPlaceMedal: return .activities + case .thirdPlaceMedal: return .activities + case .soccer: return .activities + case .baseball: return .activities + case .softball: return .activities + case .basketball: return .activities + case .volleyball: return .activities + case .football: return .activities + case .rugbyFootball: return .activities + case .tennis: return .activities + case .flyingDisc: return .activities + case .bowling: return .activities + case .cricketBatAndBall: return .activities + case .fieldHockeyStickAndBall: return .activities + case .iceHockeyStickAndPuck: return .activities + case .lacrosse: return .activities + case .tableTennisPaddleAndBall: return .activities + case .badmintonRacquetAndShuttlecock: return .activities + case .boxingGlove: return .activities + case .martialArtsUniform: return .activities + case .goalNet: return .activities + case .golf: return .activities + case .iceSkate: return .activities + case .fishingPoleAndFish: return .activities + case .divingMask: return .activities + case .runningShirtWithSash: return .activities + case .ski: return .activities + case .sled: return .activities + case .curlingStone: return .activities + case .dart: return .activities + case .yoYo: return .activities + case .kite: return .activities + case .eightBall: return .activities + case .crystalBall: return .activities + case .magicWand: return .activities + case .nazarAmulet: return .activities + case .hamsa: return .activities + case .videoGame: return .activities + case .joystick: return .activities + case .slotMachine: return .activities + case .gameDie: return .activities + case .jigsaw: return .activities + case .teddyBear: return .activities + case .pinata: return .activities + case .mirrorBall: return .activities + case .nestingDolls: return .activities + case .spades: return .activities + case .hearts: return .activities + case .diamonds: return .activities + case .clubs: return .activities + case .chessPawn: return .activities + case .blackJoker: return .activities + case .mahjong: return .activities + case .flowerPlayingCards: return .activities + case .performingArts: return .activities + case .frameWithPicture: return .activities + case .art: return .activities + case .thread: return .activities + case .sewingNeedle: return .activities + case .yarn: return .activities + case .knot: return .activities + case .eyeglasses: return .objects + case .darkSunglasses: return .objects + case .goggles: return .objects + case .labCoat: return .objects + case .safetyVest: return .objects + case .necktie: return .objects + case .shirt: return .objects + case .jeans: return .objects + case .scarf: return .objects + case .gloves: return .objects + case .coat: return .objects + case .socks: return .objects + case .dress: return .objects + case .kimono: return .objects + case .sari: return .objects + case .onePieceSwimsuit: return .objects + case .briefs: return .objects + case .shorts: return .objects + case .bikini: return .objects + case .womansClothes: return .objects + case .purse: return .objects + case .handbag: return .objects + case .pouch: return .objects + case .shoppingBags: return .objects + case .schoolSatchel: return .objects + case .thongSandal: return .objects + case .mansShoe: return .objects + case .athleticShoe: return .objects + case .hikingBoot: return .objects + case .womansFlatShoe: return .objects + case .highHeel: return .objects + case .sandal: return .objects + case .balletShoes: return .objects + case .boot: return .objects + case .crown: return .objects + case .womansHat: return .objects + case .tophat: return .objects + case .mortarBoard: return .objects + case .billedCap: return .objects + case .militaryHelmet: return .objects + case .helmetWithWhiteCross: return .objects + case .prayerBeads: return .objects + case .lipstick: return .objects + case .ring: return .objects + case .gem: return .objects + case .mute: return .objects + case .speaker: return .objects + case .sound: return .objects + case .loudSound: return .objects + case .loudspeaker: return .objects + case .mega: return .objects + case .postalHorn: return .objects + case .bell: return .objects + case .noBell: return .objects + case .musicalScore: return .objects + case .musicalNote: return .objects + case .notes: return .objects + case .studioMicrophone: return .objects + case .levelSlider: return .objects + case .controlKnobs: return .objects + case .microphone: return .objects + case .headphones: return .objects + case .radio: return .objects + case .saxophone: return .objects + case .accordion: return .objects + case .guitar: return .objects + case .musicalKeyboard: return .objects + case .trumpet: return .objects + case .violin: return .objects + case .banjo: return .objects + case .drumWithDrumsticks: return .objects + case .longDrum: return .objects + case .iphone: return .objects + case .calling: return .objects + case .phone: return .objects + case .telephoneReceiver: return .objects + case .pager: return .objects + case .fax: return .objects + case .battery: return .objects + case .lowBattery: return .objects + case .electricPlug: return .objects + case .computer: return .objects + case .desktopComputer: return .objects + case .printer: return .objects + case .keyboard: return .objects + case .threeButtonMouse: return .objects + case .trackball: return .objects + case .minidisc: return .objects + case .floppyDisk: return .objects + case .cd: return .objects + case .dvd: return .objects + case .abacus: return .objects + case .movieCamera: return .objects + case .filmFrames: return .objects + case .filmProjector: return .objects + case .clapper: return .objects + case .tv: return .objects + case .camera: return .objects + case .cameraWithFlash: return .objects + case .videoCamera: return .objects + case .vhs: return .objects + case .mag: return .objects + case .magRight: return .objects + case .candle: return .objects + case .bulb: return .objects + case .flashlight: return .objects + case .izakayaLantern: return .objects + case .diyaLamp: return .objects + case .notebookWithDecorativeCover: return .objects + case .closedBook: return .objects + case .book: return .objects + case .greenBook: return .objects + case .blueBook: return .objects + case .orangeBook: return .objects + case .books: return .objects + case .notebook: return .objects + case .ledger: return .objects + case .pageWithCurl: return .objects + case .scroll: return .objects + case .pageFacingUp: return .objects + case .newspaper: return .objects + case .rolledUpNewspaper: return .objects + case .bookmarkTabs: return .objects + case .bookmark: return .objects + case .label: return .objects + case .moneybag: return .objects + case .coin: return .objects + case .yen: return .objects + case .dollar: return .objects + case .euro: return .objects + case .pound: return .objects + case .moneyWithWings: return .objects + case .creditCard: return .objects + case .receipt: return .objects + case .chart: return .objects + case .email: return .objects + case .eMail: return .objects + case .incomingEnvelope: return .objects + case .envelopeWithArrow: return .objects + case .outboxTray: return .objects + case .inboxTray: return .objects + case .package: return .objects + case .mailbox: return .objects + case .mailboxClosed: return .objects + case .mailboxWithMail: return .objects + case .mailboxWithNoMail: return .objects + case .postbox: return .objects + case .ballotBoxWithBallot: return .objects + case .pencil2: return .objects + case .blackNib: return .objects + case .lowerLeftFountainPen: return .objects + case .lowerLeftBallpointPen: return .objects + case .lowerLeftPaintbrush: return .objects + case .lowerLeftCrayon: return .objects + case .memo: return .objects + case .briefcase: return .objects + case .fileFolder: return .objects + case .openFileFolder: return .objects + case .cardIndexDividers: return .objects + case .date: return .objects + case .calendar: return .objects + case .spiralNotePad: return .objects + case .spiralCalendarPad: return .objects + case .cardIndex: return .objects + case .chartWithUpwardsTrend: return .objects + case .chartWithDownwardsTrend: return .objects + case .barChart: return .objects + case .clipboard: return .objects + case .pushpin: return .objects + case .roundPushpin: return .objects + case .paperclip: return .objects + case .linkedPaperclips: return .objects + case .straightRuler: return .objects + case .triangularRuler: return .objects + case .scissors: return .objects + case .cardFileBox: return .objects + case .fileCabinet: return .objects + case .wastebasket: return .objects + case .lock: return .objects + case .unlock: return .objects + case .lockWithInkPen: return .objects + case .closedLockWithKey: return .objects + case .key: return .objects + case .oldKey: return .objects + case .hammer: return .objects + case .axe: return .objects + case .pick: return .objects + case .hammerAndPick: return .objects + case .hammerAndWrench: return .objects + case .daggerKnife: return .objects + case .crossedSwords: return .objects + case .gun: return .objects + case .boomerang: return .objects + case .bowAndArrow: return .objects + case .shield: return .objects + case .carpentrySaw: return .objects + case .wrench: return .objects + case .screwdriver: return .objects + case .nutAndBolt: return .objects + case .gear: return .objects + case .compression: return .objects + case .scales: return .objects + case .probingCane: return .objects + case .link: return .objects + case .chains: return .objects + case .hook: return .objects + case .toolbox: return .objects + case .magnet: return .objects + case .ladder: return .objects + case .alembic: return .objects + case .testTube: return .objects + case .petriDish: return .objects + case .dna: return .objects + case .microscope: return .objects + case .telescope: return .objects + case .satelliteAntenna: return .objects + case .syringe: return .objects + case .dropOfBlood: return .objects + case .pill: return .objects + case .adhesiveBandage: return .objects + case .crutch: return .objects + case .stethoscope: return .objects + case .xRay: return .objects + case .door: return .objects + case .elevator: return .objects + case .mirror: return .objects + case .window: return .objects + case .bed: return .objects + case .couchAndLamp: return .objects + case .chair: return .objects + case .toilet: return .objects + case .plunger: return .objects + case .shower: return .objects + case .bathtub: return .objects + case .mouseTrap: return .objects + case .razor: return .objects + case .lotionBottle: return .objects + case .safetyPin: return .objects + case .broom: return .objects + case .basket: return .objects + case .rollOfPaper: return .objects + case .bucket: return .objects + case .soap: return .objects + case .bubbles: return .objects + case .toothbrush: return .objects + case .sponge: return .objects + case .fireExtinguisher: return .objects + case .shoppingTrolley: return .objects + case .smoking: return .objects + case .coffin: return .objects + case .headstone: return .objects + case .funeralUrn: return .objects + case .moyai: return .objects + case .placard: return .objects + case .identificationCard: return .objects + case .atm: return .symbols + case .putLitterInItsPlace: return .symbols + case .potableWater: return .symbols + case .wheelchair: return .symbols + case .mens: return .symbols + case .womens: return .symbols + case .restroom: return .symbols + case .babySymbol: return .symbols + case .wc: return .symbols + case .passportControl: return .symbols + case .customs: return .symbols + case .baggageClaim: return .symbols + case .leftLuggage: return .symbols + case .warning: return .symbols + case .childrenCrossing: return .symbols + case .noEntry: return .symbols + case .noEntrySign: return .symbols + case .noBicycles: return .symbols + case .noSmoking: return .symbols + case .doNotLitter: return .symbols + case .nonPotableWater: return .symbols + case .noPedestrians: return .symbols + case .noMobilePhones: return .symbols + case .underage: return .symbols + case .radioactiveSign: return .symbols + case .biohazardSign: return .symbols + case .arrowUp: return .symbols + case .arrowUpperRight: return .symbols + case .arrowRight: return .symbols + case .arrowLowerRight: return .symbols + case .arrowDown: return .symbols + case .arrowLowerLeft: return .symbols + case .arrowLeft: return .symbols + case .arrowUpperLeft: return .symbols + case .arrowUpDown: return .symbols + case .leftRightArrow: return .symbols + case .leftwardsArrowWithHook: return .symbols + case .arrowRightHook: return .symbols + case .arrowHeadingUp: return .symbols + case .arrowHeadingDown: return .symbols + case .arrowsClockwise: return .symbols + case .arrowsCounterclockwise: return .symbols + case .back: return .symbols + case .end: return .symbols + case .on: return .symbols + case .soon: return .symbols + case .top: return .symbols + case .placeOfWorship: return .symbols + case .atomSymbol: return .symbols + case .omSymbol: return .symbols + case .starOfDavid: return .symbols + case .wheelOfDharma: return .symbols + case .yinYang: return .symbols + case .latinCross: return .symbols + case .orthodoxCross: return .symbols + case .starAndCrescent: return .symbols + case .peaceSymbol: return .symbols + case .menorahWithNineBranches: return .symbols + case .sixPointedStar: return .symbols + case .aries: return .symbols + case .taurus: return .symbols + case .gemini: return .symbols + case .cancer: return .symbols + case .leo: return .symbols + case .virgo: return .symbols + case .libra: return .symbols + case .scorpius: return .symbols + case .sagittarius: return .symbols + case .capricorn: return .symbols + case .aquarius: return .symbols + case .pisces: return .symbols + case .ophiuchus: return .symbols + case .twistedRightwardsArrows: return .symbols + case .`repeat`: return .symbols + case .repeatOne: return .symbols + case .arrowForward: return .symbols + case .fastForward: return .symbols + case .blackRightPointingDoubleTriangleWithVerticalBar: return .symbols + case .blackRightPointingTriangleWithDoubleVerticalBar: return .symbols + case .arrowBackward: return .symbols + case .rewind: return .symbols + case .blackLeftPointingDoubleTriangleWithVerticalBar: return .symbols + case .arrowUpSmall: return .symbols + case .arrowDoubleUp: return .symbols + case .arrowDownSmall: return .symbols + case .arrowDoubleDown: return .symbols + case .doubleVerticalBar: return .symbols + case .blackSquareForStop: return .symbols + case .blackCircleForRecord: return .symbols + case .eject: return .symbols + case .cinema: return .symbols + case .lowBrightness: return .symbols + case .highBrightness: return .symbols + case .signalStrength: return .symbols + case .vibrationMode: return .symbols + case .mobilePhoneOff: return .symbols + case .femaleSign: return .symbols + case .maleSign: return .symbols + case .transgenderSymbol: return .symbols + case .heavyMultiplicationX: return .symbols + case .heavyPlusSign: return .symbols + case .heavyMinusSign: return .symbols + case .heavyDivisionSign: return .symbols + case .heavyEqualsSign: return .symbols + case .infinity: return .symbols + case .bangbang: return .symbols + case .interrobang: return .symbols + case .question: return .symbols + case .greyQuestion: return .symbols + case .greyExclamation: return .symbols + case .exclamation: return .symbols + case .wavyDash: return .symbols + case .currencyExchange: return .symbols + case .heavyDollarSign: return .symbols + case .medicalSymbol: return .symbols + case .recycle: return .symbols + case .fleurDeLis: return .symbols + case .trident: return .symbols + case .nameBadge: return .symbols + case .beginner: return .symbols + case .o: return .symbols + case .whiteCheckMark: return .symbols + case .ballotBoxWithCheck: return .symbols + case .heavyCheckMark: return .symbols + case .x: return .symbols + case .negativeSquaredCrossMark: return .symbols + case .curlyLoop: return .symbols + case .loop: return .symbols + case .partAlternationMark: return .symbols + case .eightSpokedAsterisk: return .symbols + case .eightPointedBlackStar: return .symbols + case .sparkle: return .symbols + case .copyright: return .symbols + case .registered: return .symbols + case .tm: return .symbols + case .hash: return .symbols + case .keycapStar: return .symbols + case .zero: return .symbols + case .one: return .symbols + case .two: return .symbols + case .three: return .symbols + case .four: return .symbols + case .five: return .symbols + case .six: return .symbols + case .seven: return .symbols + case .eight: return .symbols + case .nine: return .symbols + case .keycapTen: return .symbols + case .capitalAbcd: return .symbols + case .abcd: return .symbols + case .oneTwoThreeFour: return .symbols + case .symbols: return .symbols + case .abc: return .symbols + case .a: return .symbols + case .ab: return .symbols + case .b: return .symbols + case .cl: return .symbols + case .cool: return .symbols + case .free: return .symbols + case .informationSource: return .symbols + case .id: return .symbols + case .m: return .symbols + case .new: return .symbols + case .ng: return .symbols + case .o2: return .symbols + case .ok: return .symbols + case .parking: return .symbols + case .sos: return .symbols + case .up: return .symbols + case .vs: return .symbols + case .koko: return .symbols + case .sa: return .symbols + case .u6708: return .symbols + case .u6709: return .symbols + case .u6307: return .symbols + case .ideographAdvantage: return .symbols + case .u5272: return .symbols + case .u7121: return .symbols + case .u7981: return .symbols + case .accept: return .symbols + case .u7533: return .symbols + case .u5408: return .symbols + case .u7a7a: return .symbols + case .congratulations: return .symbols + case .secret: return .symbols + case .u55b6: return .symbols + case .u6e80: return .symbols + case .redCircle: return .symbols + case .largeOrangeCircle: return .symbols + case .largeYellowCircle: return .symbols + case .largeGreenCircle: return .symbols + case .largeBlueCircle: return .symbols + case .largePurpleCircle: return .symbols + case .largeBrownCircle: return .symbols + case .blackCircle: return .symbols + case .whiteCircle: return .symbols + case .largeRedSquare: return .symbols + case .largeOrangeSquare: return .symbols + case .largeYellowSquare: return .symbols + case .largeGreenSquare: return .symbols + case .largeBlueSquare: return .symbols + case .largePurpleSquare: return .symbols + case .largeBrownSquare: return .symbols + case .blackLargeSquare: return .symbols + case .whiteLargeSquare: return .symbols + case .blackMediumSquare: return .symbols + case .whiteMediumSquare: return .symbols + case .blackMediumSmallSquare: return .symbols + case .whiteMediumSmallSquare: return .symbols + case .blackSmallSquare: return .symbols + case .whiteSmallSquare: return .symbols + case .largeOrangeDiamond: return .symbols + case .largeBlueDiamond: return .symbols + case .smallOrangeDiamond: return .symbols + case .smallBlueDiamond: return .symbols + case .smallRedTriangle: return .symbols + case .smallRedTriangleDown: return .symbols + case .diamondShapeWithADotInside: return .symbols + case .radioButton: return .symbols + case .whiteSquareButton: return .symbols + case .blackSquareButton: return .symbols + case .checkeredFlag: return .flags + case .triangularFlagOnPost: return .flags + case .crossedFlags: return .flags + case .wavingBlackFlag: return .flags + case .wavingWhiteFlag: return .flags + case .rainbowFlag: return .flags + case .transgenderFlag: return .flags + case .pirateFlag: return .flags + case .flagAc: return .flags + case .flagAd: return .flags + case .flagAe: return .flags + case .flagAf: return .flags + case .flagAg: return .flags + case .flagAi: return .flags + case .flagAl: return .flags + case .flagAm: return .flags + case .flagAo: return .flags + case .flagAq: return .flags + case .flagAr: return .flags + case .flagAs: return .flags + case .flagAt: return .flags + case .flagAu: return .flags + case .flagAw: return .flags + case .flagAx: return .flags + case .flagAz: return .flags + case .flagBa: return .flags + case .flagBb: return .flags + case .flagBd: return .flags + case .flagBe: return .flags + case .flagBf: return .flags + case .flagBg: return .flags + case .flagBh: return .flags + case .flagBi: return .flags + case .flagBj: return .flags + case .flagBl: return .flags + case .flagBm: return .flags + case .flagBn: return .flags + case .flagBo: return .flags + case .flagBq: return .flags + case .flagBr: return .flags + case .flagBs: return .flags + case .flagBt: return .flags + case .flagBv: return .flags + case .flagBw: return .flags + case .flagBy: return .flags + case .flagBz: return .flags + case .flagCa: return .flags + case .flagCc: return .flags + case .flagCd: return .flags + case .flagCf: return .flags + case .flagCg: return .flags + case .flagCh: return .flags + case .flagCi: return .flags + case .flagCk: return .flags + case .flagCl: return .flags + case .flagCm: return .flags + case .cn: return .flags + case .flagCo: return .flags + case .flagCp: return .flags + case .flagCr: return .flags + case .flagCu: return .flags + case .flagCv: return .flags + case .flagCw: return .flags + case .flagCx: return .flags + case .flagCy: return .flags + case .flagCz: return .flags + case .de: return .flags + case .flagDg: return .flags + case .flagDj: return .flags + case .flagDk: return .flags + case .flagDm: return .flags + case .flagDo: return .flags + case .flagDz: return .flags + case .flagEa: return .flags + case .flagEc: return .flags + case .flagEe: return .flags + case .flagEg: return .flags + case .flagEh: return .flags + case .flagEr: return .flags + case .es: return .flags + case .flagEt: return .flags + case .flagEu: return .flags + case .flagFi: return .flags + case .flagFj: return .flags + case .flagFk: return .flags + case .flagFm: return .flags + case .flagFo: return .flags + case .fr: return .flags + case .flagGa: return .flags + case .gb: return .flags + case .flagGd: return .flags + case .flagGe: return .flags + case .flagGf: return .flags + case .flagGg: return .flags + case .flagGh: return .flags + case .flagGi: return .flags + case .flagGl: return .flags + case .flagGm: return .flags + case .flagGn: return .flags + case .flagGp: return .flags + case .flagGq: return .flags + case .flagGr: return .flags + case .flagGs: return .flags + case .flagGt: return .flags + case .flagGu: return .flags + case .flagGw: return .flags + case .flagGy: return .flags + case .flagHk: return .flags + case .flagHm: return .flags + case .flagHn: return .flags + case .flagHr: return .flags + case .flagHt: return .flags + case .flagHu: return .flags + case .flagIc: return .flags + case .flagId: return .flags + case .flagIe: return .flags + case .flagIl: return .flags + case .flagIm: return .flags + case .flagIn: return .flags + case .flagIo: return .flags + case .flagIq: return .flags + case .flagIr: return .flags + case .flagIs: return .flags + case .it: return .flags + case .flagJe: return .flags + case .flagJm: return .flags + case .flagJo: return .flags + case .jp: return .flags + case .flagKe: return .flags + case .flagKg: return .flags + case .flagKh: return .flags + case .flagKi: return .flags + case .flagKm: return .flags + case .flagKn: return .flags + case .flagKp: return .flags + case .kr: return .flags + case .flagKw: return .flags + case .flagKy: return .flags + case .flagKz: return .flags + case .flagLa: return .flags + case .flagLb: return .flags + case .flagLc: return .flags + case .flagLi: return .flags + case .flagLk: return .flags + case .flagLr: return .flags + case .flagLs: return .flags + case .flagLt: return .flags + case .flagLu: return .flags + case .flagLv: return .flags + case .flagLy: return .flags + case .flagMa: return .flags + case .flagMc: return .flags + case .flagMd: return .flags + case .flagMe: return .flags + case .flagMf: return .flags + case .flagMg: return .flags + case .flagMh: return .flags + case .flagMk: return .flags + case .flagMl: return .flags + case .flagMm: return .flags + case .flagMn: return .flags + case .flagMo: return .flags + case .flagMp: return .flags + case .flagMq: return .flags + case .flagMr: return .flags + case .flagMs: return .flags + case .flagMt: return .flags + case .flagMu: return .flags + case .flagMv: return .flags + case .flagMw: return .flags + case .flagMx: return .flags + case .flagMy: return .flags + case .flagMz: return .flags + case .flagNa: return .flags + case .flagNc: return .flags + case .flagNe: return .flags + case .flagNf: return .flags + case .flagNg: return .flags + case .flagNi: return .flags + case .flagNl: return .flags + case .flagNo: return .flags + case .flagNp: return .flags + case .flagNr: return .flags + case .flagNu: return .flags + case .flagNz: return .flags + case .flagOm: return .flags + case .flagPa: return .flags + case .flagPe: return .flags + case .flagPf: return .flags + case .flagPg: return .flags + case .flagPh: return .flags + case .flagPk: return .flags + case .flagPl: return .flags + case .flagPm: return .flags + case .flagPn: return .flags + case .flagPr: return .flags + case .flagPs: return .flags + case .flagPt: return .flags + case .flagPw: return .flags + case .flagPy: return .flags + case .flagQa: return .flags + case .flagRe: return .flags + case .flagRo: return .flags + case .flagRs: return .flags + case .ru: return .flags + case .flagRw: return .flags + case .flagSa: return .flags + case .flagSb: return .flags + case .flagSc: return .flags + case .flagSd: return .flags + case .flagSe: return .flags + case .flagSg: return .flags + case .flagSh: return .flags + case .flagSi: return .flags + case .flagSj: return .flags + case .flagSk: return .flags + case .flagSl: return .flags + case .flagSm: return .flags + case .flagSn: return .flags + case .flagSo: return .flags + case .flagSr: return .flags + case .flagSs: return .flags + case .flagSt: return .flags + case .flagSv: return .flags + case .flagSx: return .flags + case .flagSy: return .flags + case .flagSz: return .flags + case .flagTa: return .flags + case .flagTc: return .flags + case .flagTd: return .flags + case .flagTf: return .flags + case .flagTg: return .flags + case .flagTh: return .flags + case .flagTj: return .flags + case .flagTk: return .flags + case .flagTl: return .flags + case .flagTm: return .flags + case .flagTn: return .flags + case .flagTo: return .flags + case .flagTr: return .flags + case .flagTt: return .flags + case .flagTv: return .flags + case .flagTw: return .flags + case .flagTz: return .flags + case .flagUa: return .flags + case .flagUg: return .flags + case .flagUm: return .flags + case .flagUn: return .flags + case .us: return .flags + case .flagUy: return .flags + case .flagUz: return .flags + case .flagVa: return .flags + case .flagVc: return .flags + case .flagVe: return .flags + case .flagVg: return .flags + case .flagVi: return .flags + case .flagVn: return .flags + case .flagVu: return .flags + case .flagWf: return .flags + case .flagWs: return .flags + case .flagXk: return .flags + case .flagYe: return .flags + case .flagYt: return .flags + case .flagZa: return .flags + case .flagZm: return .flags + case .flagZw: return .flags + case .flagEngland: return .flags + case .flagScotland: return .flags + case .flagWales: return .flags + default: fatalError("Unexpected case \(self)") + } + } + + var isNormalized: Bool { normalized == self } + var normalized: Emoji { + switch self { + case .flagUm: return .us + default: return self + } + } +} diff --git a/Session/Emoji/Emoji+Name.swift b/Session/Emoji/Emoji+Name.swift new file mode 100644 index 000000000..6acfa85e2 --- /dev/null +++ b/Session/Emoji/Emoji+Name.swift @@ -0,0 +1,1863 @@ + +// This file is generated by EmojiGenerator.swift, do not manually edit it. + +extension Emoji { + var name: String { + switch self { + case .grinning: return "grinning, grinning face" + case .smiley: return "smiley, smiling face with open mouth" + case .smile: return "smile, smiling face with open mouth and smiling eyes" + case .grin: return "grin, grinning face with smiling eyes" + case .laughing: return "smiling face with open mouth and tightly-closed eyes, laughing, satisfied" + case .sweatSmile: return "sweatsmile, sweat_smile, smiling face with open mouth and cold sweat" + case .rollingOnTheFloorLaughing: return "rolling_on_the_floor_laughing, rollingonthefloorlaughing, rolling on the floor laughing" + case .joy: return "joy, face with tears of joy" + case .slightlySmilingFace: return "slightly smiling face, slightlysmilingface, slightly_smiling_face" + case .upsideDownFace: return "upsidedownface, upside_down_face, upside-down face" + case .meltingFace: return "meltingface, melting_face, melting face" + case .wink: return "wink, winking face" + case .blush: return "blush, smiling face with smiling eyes" + case .innocent: return "innocent, smiling face with halo" + case .smilingFaceWith3Hearts: return "smiling face with smiling eyes and three hearts, smiling_face_with_3_hearts, smilingfacewith3hearts" + case .heartEyes: return "smiling face with heart-shaped eyes, hearteyes, heart_eyes" + case .starStruck: return "starstruck, grinning_face_with_star_eyes, star-struck, grinning face with star eyes" + case .kissingHeart: return "kissing_heart, face throwing a kiss, kissingheart" + case .kissing: return "kissing face, kissing" + case .relaxed: return "white smiling face, relaxed" + case .kissingClosedEyes: return "kissing_closed_eyes, kissing face with closed eyes, kissingclosedeyes" + case .kissingSmilingEyes: return "kissing face with smiling eyes, kissing_smiling_eyes, kissingsmilingeyes" + case .smilingFaceWithTear: return "smilingfacewithtear, smiling face with tear, smiling_face_with_tear" + case .yum: return "yum, face savouring delicious food" + case .stuckOutTongue: return "stuck_out_tongue, face with stuck-out tongue, stuckouttongue" + case .stuckOutTongueWinkingEye: return "stuckouttonguewinkingeye, stuck_out_tongue_winking_eye, face with stuck-out tongue and winking eye" + case .zanyFace: return "grinning_face_with_one_large_and_one_small_eye, zany_face, grinning face with one large and one small eye, zanyface" + case .stuckOutTongueClosedEyes: return "stuckouttongueclosedeyes, stuck_out_tongue_closed_eyes, face with stuck-out tongue and tightly-closed eyes" + case .moneyMouthFace: return "moneymouthface, money_mouth_face, money-mouth face" + case .huggingFace: return "huggingface, hugging_face, hugging face" + case .faceWithHandOverMouth: return "face_with_hand_over_mouth, smiling face with smiling eyes and hand covering mouth, smiling_face_with_smiling_eyes_and_hand_covering_mouth, facewithhandovermouth" + case .faceWithOpenEyesAndHandOverMouth: return "face with open eyes and hand over mouth, facewithopeneyesandhandovermouth, face_with_open_eyes_and_hand_over_mouth" + case .faceWithPeekingEye: return "face_with_peeking_eye, face with peeking eye, facewithpeekingeye" + case .shushingFace: return "shushing_face, face_with_finger_covering_closed_lips, shushingface, face with finger covering closed lips" + case .thinkingFace: return "thinkingface, thinking face, thinking_face" + case .salutingFace: return "saluting_face, saluting face, salutingface" + case .zipperMouthFace: return "zippermouthface, zipper-mouth face, zipper_mouth_face" + case .faceWithRaisedEyebrow: return "face with one eyebrow raised, face_with_one_eyebrow_raised, face_with_raised_eyebrow, facewithraisedeyebrow" + case .neutralFace: return "neutralface, neutral_face, neutral face" + case .expressionless: return "expressionless, expressionless face" + case .noMouth: return "no_mouth, face without mouth, nomouth" + case .dottedLineFace: return "dotted_line_face, dotted line face, dottedlineface" + case .faceInClouds: return "face in clouds, face_in_clouds, faceinclouds" + case .smirk: return "smirk, smirking face" + case .unamused: return "unamused face, unamused" + case .faceWithRollingEyes: return "face_with_rolling_eyes, face with rolling eyes, facewithrollingeyes" + case .grimacing: return "grimacing, grimacing face" + case .faceExhaling: return "face_exhaling, face exhaling, faceexhaling" + case .lyingFace: return "lying_face, lying face, lyingface" + case .relieved: return "relieved, relieved face" + case .pensive: return "pensive face, pensive" + case .sleepy: return "sleepy, sleepy face" + case .droolingFace: return "drooling_face, drooling face, droolingface" + case .sleeping: return "sleeping, sleeping face" + case .mask: return "face with medical mask, mask" + case .faceWithThermometer: return "face with thermometer, face_with_thermometer, facewiththermometer" + case .faceWithHeadBandage: return "face with head-bandage, face_with_head_bandage, facewithheadbandage" + case .nauseatedFace: return "nauseatedface, nauseated face, nauseated_face" + case .faceVomiting: return "face_with_open_mouth_vomiting, face_vomiting, facevomiting, face with open mouth vomiting" + case .sneezingFace: return "sneezingface, sneezing face, sneezing_face" + case .hotFace: return "hot_face, overheated face, hotface" + case .coldFace: return "freezing face, cold_face, coldface" + case .woozyFace: return "woozy_face, face with uneven eyes and wavy mouth, woozyface" + case .dizzyFace: return "dizzy_face, dizzy face, dizzyface" + case .faceWithSpiralEyes: return "face with spiral eyes, facewithspiraleyes, face_with_spiral_eyes" + case .explodingHead: return "shocked_face_with_exploding_head, explodinghead, shocked face with exploding head, exploding_head" + case .faceWithCowboyHat: return "face_with_cowboy_hat, face with cowboy hat, facewithcowboyhat" + case .partyingFace: return "partyingface, partying_face, face with party horn and party hat" + case .disguisedFace: return "disguised_face, disguisedface, disguised face" + case .sunglasses: return "sunglasses, smiling face with sunglasses" + case .nerdFace: return "nerd face, nerd_face, nerdface" + case .faceWithMonocle: return "face_with_monocle, face with monocle, facewithmonocle" + case .confused: return "confused face, confused" + case .faceWithDiagonalMouth: return "face with diagonal mouth, facewithdiagonalmouth, face_with_diagonal_mouth" + case .worried: return "worried, worried face" + case .slightlyFrowningFace: return "slightly_frowning_face, slightlyfrowningface, slightly frowning face" + case .whiteFrowningFace: return "whitefrowningface, white_frowning_face, frowning face" + case .openMouth: return "face with open mouth, open_mouth, openmouth" + case .hushed: return "hushed face, hushed" + case .astonished: return "astonished face, astonished" + case .flushed: return "flushed, flushed face" + case .pleadingFace: return "face with pleading eyes, pleading_face, pleadingface" + case .faceHoldingBackTears: return "faceholdingbacktears, face_holding_back_tears, face holding back tears" + case .frowning: return "frowning face with open mouth, frowning" + case .anguished: return "anguished, anguished face" + case .fearful: return "fearful, fearful face" + case .coldSweat: return "coldsweat, cold_sweat, face with open mouth and cold sweat" + case .disappointedRelieved: return "disappointed but relieved face, disappointedrelieved, disappointed_relieved" + case .cry: return "crying face, cry" + case .sob: return "sob, loudly crying face" + case .scream: return "face screaming in fear, scream" + case .confounded: return "confounded, confounded face" + case .persevere: return "persevere, persevering face" + case .disappointed: return "disappointed, disappointed face" + case .sweat: return "sweat, face with cold sweat" + case .weary: return "weary, weary face" + case .tiredFace: return "tired face, tired_face, tiredface" + case .yawningFace: return "yawningface, yawning face, yawning_face" + case .triumph: return "face with look of triumph, triumph" + case .rage: return "rage, pouting face" + case .angry: return "angry, angry face" + case .faceWithSymbolsOnMouth: return "serious_face_with_symbols_covering_mouth, serious face with symbols covering mouth, face_with_symbols_on_mouth, facewithsymbolsonmouth" + case .smilingImp: return "smilingimp, smiling face with horns, smiling_imp" + case .imp: return "imp" + case .skull: return "skull" + case .skullAndCrossbones: return "skull and crossbones, skull_and_crossbones, skullandcrossbones" + case .hankey: return "pile of poo, shit, poop, hankey" + case .clownFace: return "clown face, clown_face, clownface" + case .japaneseOgre: return "japanese_ogre, japaneseogre, japanese ogre" + case .japaneseGoblin: return "japanese goblin, japanese_goblin, japanesegoblin" + case .ghost: return "ghost" + case .alien: return "alien, extraterrestrial alien" + case .spaceInvader: return "spaceinvader, alien monster, space_invader" + case .robotFace: return "robot_face, robot face, robotface" + case .smileyCat: return "smiley_cat, smileycat, smiling cat face with open mouth" + case .smileCat: return "smilecat, grinning cat face with smiling eyes, smile_cat" + case .joyCat: return "joy_cat, joycat, cat face with tears of joy" + case .heartEyesCat: return "heart_eyes_cat, smiling cat face with heart-shaped eyes, hearteyescat" + case .smirkCat: return "smirk_cat, cat face with wry smile, smirkcat" + case .kissingCat: return "kissing cat face with closed eyes, kissing_cat, kissingcat" + case .screamCat: return "scream_cat, weary cat face, screamcat" + case .cryingCatFace: return "crying cat face, crying_cat_face, cryingcatface" + case .poutingCat: return "pouting_cat, poutingcat, pouting cat face" + case .seeNoEvil: return "see_no_evil, see-no-evil monkey, seenoevil" + case .hearNoEvil: return "hearnoevil, hear-no-evil monkey, hear_no_evil" + case .speakNoEvil: return "speak_no_evil, speaknoevil, speak-no-evil monkey" + case .kiss: return "kiss mark, kiss" + case .loveLetter: return "love letter, loveletter, love_letter" + case .cupid: return "heart with arrow, cupid" + case .giftHeart: return "heart with ribbon, gift_heart, giftheart" + case .sparklingHeart: return "sparklingheart, sparkling_heart, sparkling heart" + case .heartpulse: return "growing heart, heartpulse" + case .heartbeat: return "heartbeat, beating heart" + case .revolvingHearts: return "revolving hearts, revolvinghearts, revolving_hearts" + case .twoHearts: return "two hearts, twohearts, two_hearts" + case .heartDecoration: return "heart_decoration, heart decoration, heartdecoration" + case .heavyHeartExclamationMarkOrnament: return "heavy_heart_exclamation_mark_ornament, heart exclamation, heavyheartexclamationmarkornament" + case .brokenHeart: return "brokenheart, broken heart, broken_heart" + case .heartOnFire: return "heartonfire, heart on fire, heart_on_fire" + case .mendingHeart: return "mending_heart, mending heart, mendingheart" + case .heart: return "heavy black heart, heart" + case .orangeHeart: return "orange_heart, orangeheart, orange heart" + case .yellowHeart: return "yellow_heart, yellow heart, yellowheart" + case .greenHeart: return "greenheart, green heart, green_heart" + case .blueHeart: return "blue heart, blueheart, blue_heart" + case .purpleHeart: return "purpleheart, purple_heart, purple heart" + case .brownHeart: return "brown_heart, brownheart, brown heart" + case .blackHeart: return "blackheart, black_heart, black heart" + case .whiteHeart: return "white heart, whiteheart, white_heart" + case .oneHundred: return "hundred points symbol, 100, onehundred" + case .anger: return "anger symbol, anger" + case .boom: return "collision, boom, collision symbol" + case .dizzy: return "dizzy, dizzy symbol" + case .sweatDrops: return "splashing sweat symbol, sweatdrops, sweat_drops" + case .dash: return "dash symbol, dash" + case .hole: return "hole" + case .bomb: return "bomb" + case .speechBalloon: return "speech_balloon, speech balloon, speechballoon" + case .eyeInSpeechBubble: return "eyeinspeechbubble, eye-in-speech-bubble, eye in speech bubble" + case .leftSpeechBubble: return "left_speech_bubble, left speech bubble, leftspeechbubble" + case .rightAngerBubble: return "right anger bubble, rightangerbubble, right_anger_bubble" + case .thoughtBalloon: return "thought_balloon, thoughtballoon, thought balloon" + case .zzz: return "zzz, sleeping symbol" + case .wave: return "waving hand sign, wave" + case .raisedBackOfHand: return "raised_back_of_hand, raised back of hand, raisedbackofhand" + case .raisedHandWithFingersSplayed: return "raisedhandwithfingerssplayed, hand with fingers splayed, raised_hand_with_fingers_splayed" + case .hand: return "raised_hand, raised hand, hand" + case .spockHand: return "raised hand with part between middle and ring fingers, spockhand, spock-hand" + case .rightwardsHand: return "rightwardshand, rightwards hand, rightwards_hand" + case .leftwardsHand: return "leftwards hand, leftwardshand, leftwards_hand" + case .palmDownHand: return "palmdownhand, palm_down_hand, palm down hand" + case .palmUpHand: return "palmuphand, palm_up_hand, palm up hand" + case .okHand: return "ok hand sign, okhand, ok_hand" + case .pinchedFingers: return "pinched_fingers, pinchedfingers, pinched fingers" + case .pinchingHand: return "pinching hand, pinchinghand, pinching_hand" + case .v: return "v, victory hand" + case .crossedFingers: return "crossedfingers, hand_with_index_and_middle_fingers_crossed, hand with index and middle fingers crossed, crossed_fingers" + case .handWithIndexFingerAndThumbCrossed: return "hand_with_index_finger_and_thumb_crossed, handwithindexfingerandthumbcrossed, hand with index finger and thumb crossed" + case .iLoveYouHandSign: return "i_love_you_hand_sign, iloveyouhandsign, i love you hand sign" + case .theHorns: return "sign_of_the_horns, the_horns, thehorns, sign of the horns" + case .callMeHand: return "callmehand, call_me_hand, call me hand" + case .pointLeft: return "pointleft, point_left, white left pointing backhand index" + case .pointRight: return "white right pointing backhand index, point_right, pointright" + case .pointUp2: return "point_up_2, white up pointing backhand index, pointup2" + case .middleFinger: return "middle_finger, reversed_hand_with_middle_finger_extended, middlefinger, reversed hand with middle finger extended" + case .pointDown: return "point_down, white down pointing backhand index, pointdown" + case .pointUp: return "point_up, white up pointing index, pointup" + case .indexPointingAtTheViewer: return "index_pointing_at_the_viewer, indexpointingattheviewer, index pointing at the viewer" + case .plusOne: return "+1, thumbsup, thumbs up sign, plusone" + case .negativeOne: return "thumbsdown, -1, thumbs down sign, negativeone" + case .fist: return "fist, raised fist" + case .facepunch: return "punch, facepunch, fisted hand sign" + case .leftFacingFist: return "left-facing_fist, left-facing fist, leftfacingfist" + case .rightFacingFist: return "right-facing fist, rightfacingfist, right-facing_fist" + case .clap: return "clapping hands sign, clap" + case .raisedHands: return "person raising both hands in celebration, raised_hands, raisedhands" + case .heartHands: return "heart_hands, hearthands, heart hands" + case .openHands: return "open hands sign, open_hands, openhands" + case .palmsUpTogether: return "palms up together, palms_up_together, palmsuptogether" + case .handshake: return "handshake" + case .pray: return "pray, person with folded hands" + case .writingHand: return "writing hand, writing_hand, writinghand" + case .nailCare: return "nailcare, nail polish, nail_care" + case .selfie: return "selfie" + case .muscle: return "muscle, flexed biceps" + case .mechanicalArm: return "mechanicalarm, mechanical_arm, mechanical arm" + case .mechanicalLeg: return "mechanical leg, mechanicalleg, mechanical_leg" + case .leg: return "leg" + case .foot: return "foot" + case .ear: return "ear" + case .earWithHearingAid: return "earwithhearingaid, ear with hearing aid, ear_with_hearing_aid" + case .nose: return "nose" + case .brain: return "brain" + case .anatomicalHeart: return "anatomical heart, anatomical_heart, anatomicalheart" + case .lungs: return "lungs" + case .tooth: return "tooth" + case .bone: return "bone" + case .eyes: return "eyes" + case .eye: return "eye" + case .tongue: return "tongue" + case .lips: return "mouth, lips" + case .bitingLip: return "biting lip, bitinglip, biting_lip" + case .baby: return "baby" + case .child: return "child" + case .boy: return "boy" + case .girl: return "girl" + case .adult: return "adult" + case .personWithBlondHair: return "person with blond hair, personwithblondhair, person_with_blond_hair" + case .man: return "man" + case .beardedPerson: return "bearded_person, beardedperson, bearded person" + case .manWithBeard: return "man: beard, manwithbeard, man_with_beard" + case .womanWithBeard: return "womanwithbeard, woman_with_beard, woman: beard" + case .redHairedMan: return "man: red hair, redhairedman, red_haired_man" + case .curlyHairedMan: return "curlyhairedman, curly_haired_man, man: curly hair" + case .whiteHairedMan: return "white_haired_man, man: white hair, whitehairedman" + case .baldMan: return "baldman, man: bald, bald_man" + case .woman: return "woman" + case .redHairedWoman: return "redhairedwoman, red_haired_woman, woman: red hair" + case .redHairedPerson: return "redhairedperson, red_haired_person, person: red hair" + case .curlyHairedWoman: return "curlyhairedwoman, curly_haired_woman, woman: curly hair" + case .curlyHairedPerson: return "curlyhairedperson, person: curly hair, curly_haired_person" + case .whiteHairedWoman: return "white_haired_woman, woman: white hair, whitehairedwoman" + case .whiteHairedPerson: return "whitehairedperson, white_haired_person, person: white hair" + case .baldWoman: return "woman: bald, bald_woman, baldwoman" + case .baldPerson: return "bald_person, person: bald, baldperson" + case .blondHairedWoman: return "woman: blond hair, blondhairedwoman, blond-haired-woman" + case .blondHairedMan: return "blond-haired-man, blondhairedman, man: blond hair" + case .olderAdult: return "older_adult, older adult, olderadult" + case .olderMan: return "older_man, older man, olderman" + case .olderWoman: return "older woman, older_woman, olderwoman" + case .personFrowning: return "person_frowning, personfrowning, person frowning" + case .manFrowning: return "man frowning, manfrowning, man-frowning" + case .womanFrowning: return "woman frowning, woman-frowning, womanfrowning" + case .personWithPoutingFace: return "person with pouting face, personwithpoutingface, person_with_pouting_face" + case .manPouting: return "man pouting, man-pouting, manpouting" + case .womanPouting: return "woman-pouting, woman pouting, womanpouting" + case .noGood: return "no_good, nogood, face with no good gesture" + case .manGesturingNo: return "mangesturingno, man-gesturing-no, man gesturing no" + case .womanGesturingNo: return "woman gesturing no, womangesturingno, woman-gesturing-no" + case .okWoman: return "ok_woman, okwoman, face with ok gesture" + case .manGesturingOk: return "man gesturing ok, man-gesturing-ok, mangesturingok" + case .womanGesturingOk: return "woman-gesturing-ok, woman gesturing ok, womangesturingok" + case .informationDeskPerson: return "information desk person, informationdeskperson, information_desk_person" + case .manTippingHand: return "man-tipping-hand, man tipping hand, mantippinghand" + case .womanTippingHand: return "woman tipping hand, woman-tipping-hand, womantippinghand" + case .raisingHand: return "happy person raising one hand, raisinghand, raising_hand" + case .manRaisingHand: return "manraisinghand, man-raising-hand, man raising hand" + case .womanRaisingHand: return "woman-raising-hand, woman raising hand, womanraisinghand" + case .deafPerson: return "deafperson, deaf_person, deaf person" + case .deafMan: return "deafman, deaf_man, deaf man" + case .deafWoman: return "deaf woman, deaf_woman, deafwoman" + case .bow: return "person bowing deeply, bow" + case .manBowing: return "manbowing, man-bowing, man bowing" + case .womanBowing: return "woman-bowing, womanbowing, woman bowing" + case .facePalm: return "face palm, facepalm, face_palm" + case .manFacepalming: return "man-facepalming, man facepalming, manfacepalming" + case .womanFacepalming: return "woman facepalming, woman-facepalming, womanfacepalming" + case .shrug: return "shrug" + case .manShrugging: return "manshrugging, man-shrugging, man shrugging" + case .womanShrugging: return "woman-shrugging, womanshrugging, woman shrugging" + case .healthWorker: return "health_worker, health worker, healthworker" + case .maleDoctor: return "male-doctor, maledoctor, man health worker" + case .femaleDoctor: return "woman health worker, femaledoctor, female-doctor" + case .student: return "student" + case .maleStudent: return "male-student, malestudent, man student" + case .femaleStudent: return "femalestudent, woman student, female-student" + case .teacher: return "teacher" + case .maleTeacher: return "male-teacher, maleteacher, man teacher" + case .femaleTeacher: return "woman teacher, female-teacher, femaleteacher" + case .judge: return "judge" + case .maleJudge: return "man judge, male-judge, malejudge" + case .femaleJudge: return "female-judge, woman judge, femalejudge" + case .farmer: return "farmer" + case .maleFarmer: return "male-farmer, man farmer, malefarmer" + case .femaleFarmer: return "femalefarmer, female-farmer, woman farmer" + case .cook: return "cook" + case .maleCook: return "man cook, malecook, male-cook" + case .femaleCook: return "female-cook, woman cook, femalecook" + case .mechanic: return "mechanic" + case .maleMechanic: return "malemechanic, male-mechanic, man mechanic" + case .femaleMechanic: return "female-mechanic, woman mechanic, femalemechanic" + case .factoryWorker: return "factory_worker, factory worker, factoryworker" + case .maleFactoryWorker: return "man factory worker, male-factory-worker, malefactoryworker" + case .femaleFactoryWorker: return "female-factory-worker, woman factory worker, femalefactoryworker" + case .officeWorker: return "officeworker, office_worker, office worker" + case .maleOfficeWorker: return "male-office-worker, maleofficeworker, man office worker" + case .femaleOfficeWorker: return "female-office-worker, woman office worker, femaleofficeworker" + case .scientist: return "scientist" + case .maleScientist: return "man scientist, malescientist, male-scientist" + case .femaleScientist: return "female-scientist, femalescientist, woman scientist" + case .technologist: return "technologist" + case .maleTechnologist: return "male-technologist, maletechnologist, man technologist" + case .femaleTechnologist: return "femaletechnologist, woman technologist, female-technologist" + case .singer: return "singer" + case .maleSinger: return "male-singer, man singer, malesinger" + case .femaleSinger: return "woman singer, femalesinger, female-singer" + case .artist: return "artist" + case .maleArtist: return "man artist, maleartist, male-artist" + case .femaleArtist: return "femaleartist, female-artist, woman artist" + case .pilot: return "pilot" + case .malePilot: return "male-pilot, malepilot, man pilot" + case .femalePilot: return "female-pilot, woman pilot, femalepilot" + case .astronaut: return "astronaut" + case .maleAstronaut: return "man astronaut, male-astronaut, maleastronaut" + case .femaleAstronaut: return "femaleastronaut, female-astronaut, woman astronaut" + case .firefighter: return "firefighter" + case .maleFirefighter: return "man firefighter, male-firefighter, malefirefighter" + case .femaleFirefighter: return "woman firefighter, femalefirefighter, female-firefighter" + case .cop: return "police officer, cop" + case .malePoliceOfficer: return "malepoliceofficer, male-police-officer, man police officer" + case .femalePoliceOfficer: return "woman police officer, female-police-officer, femalepoliceofficer" + case .sleuthOrSpy: return "sleuthorspy, sleuth_or_spy, detective" + case .maleDetective: return "maledetective, male-detective, man detective" + case .femaleDetective: return "woman detective, female-detective, femaledetective" + case .guardsman: return "guardsman" + case .maleGuard: return "maleguard, male-guard, man guard" + case .femaleGuard: return "woman guard, female-guard, femaleguard" + case .ninja: return "ninja" + case .constructionWorker: return "construction_worker, constructionworker, construction worker" + case .maleConstructionWorker: return "male-construction-worker, man construction worker, maleconstructionworker" + case .femaleConstructionWorker: return "femaleconstructionworker, female-construction-worker, woman construction worker" + case .personWithCrown: return "person_with_crown, person with crown, personwithcrown" + case .prince: return "prince" + case .princess: return "princess" + case .manWithTurban: return "man with turban, manwithturban, man_with_turban" + case .manWearingTurban: return "man-wearing-turban, manwearingturban, man wearing turban" + case .womanWearingTurban: return "woman-wearing-turban, womanwearingturban, woman wearing turban" + case .manWithGuaPiMao: return "man_with_gua_pi_mao, man with gua pi mao, manwithguapimao" + case .personWithHeadscarf: return "person_with_headscarf, personwithheadscarf, person with headscarf" + case .personInTuxedo: return "person_in_tuxedo, personintuxedo, man in tuxedo" + case .manInTuxedo: return "man_in_tuxedo, manintuxedo, man in tuxedo" + case .womanInTuxedo: return "womanintuxedo, woman in tuxedo, woman_in_tuxedo" + case .brideWithVeil: return "bridewithveil, bride_with_veil, bride with veil" + case .manWithVeil: return "man_with_veil, man with veil, manwithveil" + case .womanWithVeil: return "woman with veil, womanwithveil, woman_with_veil" + case .pregnantWoman: return "pregnant woman, pregnantwoman, pregnant_woman" + case .pregnantMan: return "pregnant_man, pregnant man, pregnantman" + case .pregnantPerson: return "pregnant_person, pregnant person, pregnantperson" + case .breastFeeding: return "breast-feeding, breastfeeding" + case .womanFeedingBaby: return "womanfeedingbaby, woman_feeding_baby, woman feeding baby" + case .manFeedingBaby: return "man feeding baby, manfeedingbaby, man_feeding_baby" + case .personFeedingBaby: return "personfeedingbaby, person_feeding_baby, person feeding baby" + case .angel: return "angel, baby angel" + case .santa: return "father christmas, santa" + case .mrsClaus: return "mrsclaus, mother_christmas, mother christmas, mrs_claus" + case .mxClaus: return "mxclaus, mx claus, mx_claus" + case .superhero: return "superhero" + case .maleSuperhero: return "male_superhero, man superhero, malesuperhero" + case .femaleSuperhero: return "female_superhero, femalesuperhero, woman superhero" + case .supervillain: return "supervillain" + case .maleSupervillain: return "malesupervillain, man supervillain, male_supervillain" + case .femaleSupervillain: return "female_supervillain, woman supervillain, femalesupervillain" + case .mage: return "mage" + case .maleMage: return "male_mage, malemage, man mage" + case .femaleMage: return "female_mage, woman mage, femalemage" + case .fairy: return "fairy" + case .maleFairy: return "malefairy, man fairy, male_fairy" + case .femaleFairy: return "femalefairy, female_fairy, woman fairy" + case .vampire: return "vampire" + case .maleVampire: return "malevampire, male_vampire, man vampire" + case .femaleVampire: return "female_vampire, woman vampire, femalevampire" + case .merperson: return "merperson" + case .merman: return "merman" + case .mermaid: return "mermaid" + case .elf: return "elf" + case .maleElf: return "male_elf, man elf, maleelf" + case .femaleElf: return "female_elf, femaleelf, woman elf" + case .genie: return "genie" + case .maleGenie: return "man genie, male_genie, malegenie" + case .femaleGenie: return "woman genie, femalegenie, female_genie" + case .zombie: return "zombie" + case .maleZombie: return "malezombie, man zombie, male_zombie" + case .femaleZombie: return "woman zombie, female_zombie, femalezombie" + case .troll: return "troll" + case .massage: return "massage, face massage" + case .manGettingMassage: return "man getting massage, man-getting-massage, mangettingmassage" + case .womanGettingMassage: return "woman getting massage, womangettingmassage, woman-getting-massage" + case .haircut: return "haircut" + case .manGettingHaircut: return "mangettinghaircut, man-getting-haircut, man getting haircut" + case .womanGettingHaircut: return "woman-getting-haircut, woman getting haircut, womangettinghaircut" + case .walking: return "pedestrian, walking" + case .manWalking: return "man-walking, man walking, manwalking" + case .womanWalking: return "woman-walking, woman walking, womanwalking" + case .standingPerson: return "standing person, standingperson, standing_person" + case .manStanding: return "manstanding, man_standing, man standing" + case .womanStanding: return "woman_standing, womanstanding, woman standing" + case .kneelingPerson: return "kneelingperson, kneeling person, kneeling_person" + case .manKneeling: return "man kneeling, man_kneeling, mankneeling" + case .womanKneeling: return "woman_kneeling, woman kneeling, womankneeling" + case .personWithProbingCane: return "person_with_probing_cane, personwithprobingcane, person with white cane" + case .manWithProbingCane: return "man with white cane, manwithprobingcane, man_with_probing_cane" + case .womanWithProbingCane: return "woman_with_probing_cane, womanwithprobingcane, woman with white cane" + case .personInMotorizedWheelchair: return "personinmotorizedwheelchair, person in motorized wheelchair, person_in_motorized_wheelchair" + case .manInMotorizedWheelchair: return "man in motorized wheelchair, maninmotorizedwheelchair, man_in_motorized_wheelchair" + case .womanInMotorizedWheelchair: return "woman in motorized wheelchair, womaninmotorizedwheelchair, woman_in_motorized_wheelchair" + case .personInManualWheelchair: return "personinmanualwheelchair, person_in_manual_wheelchair, person in manual wheelchair" + case .manInManualWheelchair: return "man_in_manual_wheelchair, maninmanualwheelchair, man in manual wheelchair" + case .womanInManualWheelchair: return "womaninmanualwheelchair, woman_in_manual_wheelchair, woman in manual wheelchair" + case .runner: return "running, runner" + case .manRunning: return "man-running, man running, manrunning" + case .womanRunning: return "woman running, womanrunning, woman-running" + case .dancer: return "dancer" + case .manDancing: return "man_dancing, mandancing, man dancing" + case .manInBusinessSuitLevitating: return "maninbusinesssuitlevitating, man_in_business_suit_levitating, person in suit levitating" + case .dancers: return "dancers, woman with bunny ears" + case .menWithBunnyEarsPartying: return "menwithbunnyearspartying, man-with-bunny-ears-partying, men with bunny ears, men-with-bunny-ears-partying" + case .womenWithBunnyEarsPartying: return "women-with-bunny-ears-partying, woman-with-bunny-ears-partying, womenwithbunnyearspartying, women with bunny ears" + case .personInSteamyRoom: return "person_in_steamy_room, person in steamy room, personinsteamyroom" + case .manInSteamyRoom: return "man in steamy room, man_in_steamy_room, maninsteamyroom" + case .womanInSteamyRoom: return "woman in steamy room, woman_in_steamy_room, womaninsteamyroom" + case .personClimbing: return "person_climbing, person climbing, personclimbing" + case .manClimbing: return "man climbing, man_climbing, manclimbing" + case .womanClimbing: return "woman climbing, womanclimbing, woman_climbing" + case .fencer: return "fencer" + case .horseRacing: return "horse_racing, horseracing, horse racing" + case .skier: return "skier" + case .snowboarder: return "snowboarder" + case .golfer: return "golfer, person golfing" + case .manGolfing: return "mangolfing, man-golfing, man golfing" + case .womanGolfing: return "womangolfing, woman golfing, woman-golfing" + case .surfer: return "surfer" + case .manSurfing: return "man surfing, man-surfing, mansurfing" + case .womanSurfing: return "woman surfing, womansurfing, woman-surfing" + case .rowboat: return "rowboat" + case .manRowingBoat: return "man-rowing-boat, man rowing boat, manrowingboat" + case .womanRowingBoat: return "womanrowingboat, woman rowing boat, woman-rowing-boat" + case .swimmer: return "swimmer" + case .manSwimming: return "man swimming, manswimming, man-swimming" + case .womanSwimming: return "woman swimming, womanswimming, woman-swimming" + case .personWithBall: return "person_with_ball, person bouncing ball, personwithball" + case .manBouncingBall: return "manbouncingball, man bouncing ball, man-bouncing-ball" + case .womanBouncingBall: return "woman-bouncing-ball, woman bouncing ball, womanbouncingball" + case .weightLifter: return "person lifting weights, weight_lifter, weightlifter" + case .manLiftingWeights: return "manliftingweights, man-lifting-weights, man lifting weights" + case .womanLiftingWeights: return "woman-lifting-weights, woman lifting weights, womanliftingweights" + case .bicyclist: return "bicyclist" + case .manBiking: return "man biking, manbiking, man-biking" + case .womanBiking: return "woman biking, womanbiking, woman-biking" + case .mountainBicyclist: return "mountain_bicyclist, mountain bicyclist, mountainbicyclist" + case .manMountainBiking: return "man mountain biking, manmountainbiking, man-mountain-biking" + case .womanMountainBiking: return "woman-mountain-biking, womanmountainbiking, woman mountain biking" + case .personDoingCartwheel: return "person doing cartwheel, persondoingcartwheel, person_doing_cartwheel" + case .manCartwheeling: return "man-cartwheeling, mancartwheeling, man cartwheeling" + case .womanCartwheeling: return "woman-cartwheeling, woman cartwheeling, womancartwheeling" + case .wrestlers: return "wrestlers" + case .manWrestling: return "man-wrestling, manwrestling, men wrestling" + case .womanWrestling: return "womanwrestling, women wrestling, woman-wrestling" + case .waterPolo: return "water polo, waterpolo, water_polo" + case .manPlayingWaterPolo: return "man playing water polo, manplayingwaterpolo, man-playing-water-polo" + case .womanPlayingWaterPolo: return "womanplayingwaterpolo, woman playing water polo, woman-playing-water-polo" + case .handball: return "handball" + case .manPlayingHandball: return "man-playing-handball, manplayinghandball, man playing handball" + case .womanPlayingHandball: return "womanplayinghandball, woman playing handball, woman-playing-handball" + case .juggling: return "juggling" + case .manJuggling: return "man-juggling, manjuggling, man juggling" + case .womanJuggling: return "woman-juggling, womanjuggling, woman juggling" + case .personInLotusPosition: return "personinlotusposition, person in lotus position, person_in_lotus_position" + case .manInLotusPosition: return "maninlotusposition, man_in_lotus_position, man in lotus position" + case .womanInLotusPosition: return "woman_in_lotus_position, woman in lotus position, womaninlotusposition" + case .bath: return "bath" + case .sleepingAccommodation: return "sleeping_accommodation, sleeping accommodation, sleepingaccommodation" + case .peopleHoldingHands: return "peopleholdinghands, people_holding_hands, people holding hands" + case .twoWomenHoldingHands: return "twowomenholdinghands, two women holding hands, women_holding_hands, two_women_holding_hands" + case .manAndWomanHoldingHands: return "man and woman holding hands, couple, man_and_woman_holding_hands, manandwomanholdinghands, woman_and_man_holding_hands" + case .twoMenHoldingHands: return "two_men_holding_hands, twomenholdinghands, men_holding_hands, two men holding hands" + case .personKissPerson: return "kiss, personkissperson, couplekiss" + case .womanKissMan: return "womankissman, kiss: woman, man, woman-kiss-man" + case .manKissMan: return "mankissman, man-kiss-man, kiss: man, man" + case .womanKissWoman: return "kiss: woman, woman, womankisswoman, woman-kiss-woman" + case .personHeartPerson: return "couple_with_heart, personheartperson, couple with heart" + case .womanHeartMan: return "womanheartman, woman-heart-man, couple with heart: woman, man" + case .manHeartMan: return "man-heart-man, couple with heart: man, man, manheartman" + case .womanHeartWoman: return "couple with heart: woman, woman, womanheartwoman, woman-heart-woman" + case .family: return "family" + case .manWomanBoy: return "family: man, woman, boy, man-woman-boy, manwomanboy" + case .manWomanGirl: return "manwomangirl, man-woman-girl, family: man, woman, girl" + case .manWomanGirlBoy: return "man-woman-girl-boy, family: man, woman, girl, boy, manwomangirlboy" + case .manWomanBoyBoy: return "family: man, woman, boy, boy, man-woman-boy-boy, manwomanboyboy" + case .manWomanGirlGirl: return "man-woman-girl-girl, family: man, woman, girl, girl, manwomangirlgirl" + case .manManBoy: return "manmanboy, man-man-boy, family: man, man, boy" + case .manManGirl: return "manmangirl, family: man, man, girl, man-man-girl" + case .manManGirlBoy: return "manmangirlboy, man-man-girl-boy, family: man, man, girl, boy" + case .manManBoyBoy: return "manmanboyboy, man-man-boy-boy, family: man, man, boy, boy" + case .manManGirlGirl: return "man-man-girl-girl, family: man, man, girl, girl, manmangirlgirl" + case .womanWomanBoy: return "family: woman, woman, boy, womanwomanboy, woman-woman-boy" + case .womanWomanGirl: return "woman-woman-girl, family: woman, woman, girl, womanwomangirl" + case .womanWomanGirlBoy: return "family: woman, woman, girl, boy, womanwomangirlboy, woman-woman-girl-boy" + case .womanWomanBoyBoy: return "womanwomanboyboy, woman-woman-boy-boy, family: woman, woman, boy, boy" + case .womanWomanGirlGirl: return "family: woman, woman, girl, girl, womanwomangirlgirl, woman-woman-girl-girl" + case .manBoy: return "manboy, man-boy, family: man, boy" + case .manBoyBoy: return "man-boy-boy, family: man, boy, boy, manboyboy" + case .manGirl: return "man-girl, mangirl, family: man, girl" + case .manGirlBoy: return "man-girl-boy, family: man, girl, boy, mangirlboy" + case .manGirlGirl: return "mangirlgirl, man-girl-girl, family: man, girl, girl" + case .womanBoy: return "womanboy, woman-boy, family: woman, boy" + case .womanBoyBoy: return "family: woman, boy, boy, woman-boy-boy, womanboyboy" + case .womanGirl: return "family: woman, girl, womangirl, woman-girl" + case .womanGirlBoy: return "family: woman, girl, boy, womangirlboy, woman-girl-boy" + case .womanGirlGirl: return "woman-girl-girl, womangirlgirl, family: woman, girl, girl" + case .speakingHeadInSilhouette: return "speaking_head_in_silhouette, speaking head, speakingheadinsilhouette" + case .bustInSilhouette: return "bustinsilhouette, bust_in_silhouette, bust in silhouette" + case .bustsInSilhouette: return "busts_in_silhouette, busts in silhouette, bustsinsilhouette" + case .peopleHugging: return "people_hugging, people hugging, peoplehugging" + case .footprints: return "footprints" + case .skinTone2: return "skin-tone-2, emoji modifier fitzpatrick type-1-2, skintone2" + case .skinTone3: return "skintone3, skin-tone-3, emoji modifier fitzpatrick type-3" + case .skinTone4: return "skintone4, skin-tone-4, emoji modifier fitzpatrick type-4" + case .skinTone5: return "emoji modifier fitzpatrick type-5, skintone5, skin-tone-5" + case .skinTone6: return "skin-tone-6, emoji modifier fitzpatrick type-6, skintone6" + case .monkeyFace: return "monkeyface, monkey_face, monkey face" + case .monkey: return "monkey" + case .gorilla: return "gorilla" + case .orangutan: return "orangutan" + case .dog: return "dog face, dog" + case .dog2: return "dog, dog2" + case .guideDog: return "guide_dog, guidedog, guide dog" + case .serviceDog: return "service dog, service_dog, servicedog" + case .poodle: return "poodle" + case .wolf: return "wolf, wolf face" + case .foxFace: return "foxface, fox_face, fox face" + case .raccoon: return "raccoon" + case .cat: return "cat, cat face" + case .cat2: return "cat2, cat" + case .blackCat: return "black cat, blackcat, black_cat" + case .lionFace: return "lion_face, lion face, lionface" + case .tiger: return "tiger face, tiger" + case .tiger2: return "tiger, tiger2" + case .leopard: return "leopard" + case .horse: return "horse face, horse" + case .racehorse: return "horse, racehorse" + case .unicornFace: return "unicorn_face, unicorn face, unicornface" + case .zebraFace: return "zebra_face, zebra face, zebraface" + case .deer: return "deer" + case .bison: return "bison" + case .cow: return "cow face, cow" + case .ox: return "ox" + case .waterBuffalo: return "water buffalo, waterbuffalo, water_buffalo" + case .cow2: return "cow, cow2" + case .pig: return "pig, pig face" + case .pig2: return "pig2, pig" + case .boar: return "boar" + case .pigNose: return "pig_nose, pig nose, pignose" + case .ram: return "ram" + case .sheep: return "sheep" + case .goat: return "goat" + case .dromedaryCamel: return "dromedary_camel, dromedary camel, dromedarycamel" + case .camel: return "bactrian camel, camel" + case .llama: return "llama" + case .giraffeFace: return "giraffe face, giraffe_face, giraffeface" + case .elephant: return "elephant" + case .mammoth: return "mammoth" + case .rhinoceros: return "rhinoceros" + case .hippopotamus: return "hippopotamus" + case .mouse: return "mouse, mouse face" + case .mouse2: return "mouse2, mouse" + case .rat: return "rat" + case .hamster: return "hamster, hamster face" + case .rabbit: return "rabbit, rabbit face" + case .rabbit2: return "rabbit2, rabbit" + case .chipmunk: return "chipmunk" + case .beaver: return "beaver" + case .hedgehog: return "hedgehog" + case .bat: return "bat" + case .bear: return "bear face, bear" + case .polarBear: return "polarbear, polar_bear, polar bear" + case .koala: return "koala" + case .pandaFace: return "panda face, pandaface, panda_face" + case .sloth: return "sloth" + case .otter: return "otter" + case .skunk: return "skunk" + case .kangaroo: return "kangaroo" + case .badger: return "badger" + case .feet: return "paw prints, feet, paw_prints" + case .turkey: return "turkey" + case .chicken: return "chicken" + case .rooster: return "rooster" + case .hatchingChick: return "hatching chick, hatching_chick, hatchingchick" + case .babyChick: return "baby_chick, baby chick, babychick" + case .hatchedChick: return "front-facing baby chick, hatchedchick, hatched_chick" + case .bird: return "bird" + case .penguin: return "penguin" + case .doveOfPeace: return "dove, doveofpeace, dove_of_peace" + case .eagle: return "eagle" + case .duck: return "duck" + case .swan: return "swan" + case .owl: return "owl" + case .dodo: return "dodo" + case .feather: return "feather" + case .flamingo: return "flamingo" + case .peacock: return "peacock" + case .parrot: return "parrot" + case .frog: return "frog, frog face" + case .crocodile: return "crocodile" + case .turtle: return "turtle" + case .lizard: return "lizard" + case .snake: return "snake" + case .dragonFace: return "dragon face, dragon_face, dragonface" + case .dragon: return "dragon" + case .sauropod: return "sauropod" + case .tRex: return "t-rex, trex" + case .whale: return "spouting whale, whale" + case .whale2: return "whale, whale2" + case .dolphin: return "flipper, dolphin" + case .seal: return "seal" + case .fish: return "fish" + case .tropicalFish: return "tropical_fish, tropical fish, tropicalfish" + case .blowfish: return "blowfish" + case .shark: return "shark" + case .octopus: return "octopus" + case .shell: return "shell, spiral shell" + case .coral: return "coral" + case .snail: return "snail" + case .butterfly: return "butterfly" + case .bug: return "bug" + case .ant: return "ant" + case .bee: return "bee, honeybee" + case .beetle: return "beetle" + case .ladybug: return "lady_beetle, lady beetle, ladybug" + case .cricket: return "cricket" + case .cockroach: return "cockroach" + case .spider: return "spider" + case .spiderWeb: return "spiderweb, spider_web, spider web" + case .scorpion: return "scorpion" + case .mosquito: return "mosquito" + case .fly: return "fly" + case .worm: return "worm" + case .microbe: return "microbe" + case .bouquet: return "bouquet" + case .cherryBlossom: return "cherryblossom, cherry_blossom, cherry blossom" + case .whiteFlower: return "white_flower, white flower, whiteflower" + case .lotus: return "lotus" + case .rosette: return "rosette" + case .rose: return "rose" + case .wiltedFlower: return "wilted flower, wiltedflower, wilted_flower" + case .hibiscus: return "hibiscus" + case .sunflower: return "sunflower" + case .blossom: return "blossom" + case .tulip: return "tulip" + case .seedling: return "seedling" + case .pottedPlant: return "potted plant, potted_plant, pottedplant" + case .evergreenTree: return "evergreen tree, evergreentree, evergreen_tree" + case .deciduousTree: return "deciduous_tree, deciduous tree, deciduoustree" + case .palmTree: return "palm tree, palmtree, palm_tree" + case .cactus: return "cactus" + case .earOfRice: return "earofrice, ear of rice, ear_of_rice" + case .herb: return "herb" + case .shamrock: return "shamrock" + case .fourLeafClover: return "four leaf clover, four_leaf_clover, fourleafclover" + case .mapleLeaf: return "maple_leaf, mapleleaf, maple leaf" + case .fallenLeaf: return "fallen_leaf, fallen leaf, fallenleaf" + case .leaves: return "leaves, leaf fluttering in wind" + case .emptyNest: return "emptynest, empty_nest, empty nest" + case .nestWithEggs: return "nest_with_eggs, nest with eggs, nestwitheggs" + case .grapes: return "grapes" + case .melon: return "melon" + case .watermelon: return "watermelon" + case .tangerine: return "tangerine" + case .lemon: return "lemon" + case .banana: return "banana" + case .pineapple: return "pineapple" + case .mango: return "mango" + case .apple: return "red apple, apple" + case .greenApple: return "green apple, green_apple, greenapple" + case .pear: return "pear" + case .peach: return "peach" + case .cherries: return "cherries" + case .strawberry: return "strawberry" + case .blueberries: return "blueberries" + case .kiwifruit: return "kiwifruit" + case .tomato: return "tomato" + case .olive: return "olive" + case .coconut: return "coconut" + case .avocado: return "avocado" + case .eggplant: return "aubergine, eggplant" + case .potato: return "potato" + case .carrot: return "carrot" + case .corn: return "ear of maize, corn" + case .hotPepper: return "hot pepper, hot_pepper, hotpepper" + case .bellPepper: return "bell pepper, bellpepper, bell_pepper" + case .cucumber: return "cucumber" + case .leafyGreen: return "leafygreen, leafy_green, leafy green" + case .broccoli: return "broccoli" + case .garlic: return "garlic" + case .onion: return "onion" + case .mushroom: return "mushroom" + case .peanuts: return "peanuts" + case .beans: return "beans" + case .chestnut: return "chestnut" + case .bread: return "bread" + case .croissant: return "croissant" + case .baguetteBread: return "baguette bread, baguette_bread, baguettebread" + case .flatbread: return "flatbread" + case .pretzel: return "pretzel" + case .bagel: return "bagel" + case .pancakes: return "pancakes" + case .waffle: return "waffle" + case .cheeseWedge: return "cheesewedge, cheese wedge, cheese_wedge" + case .meatOnBone: return "meatonbone, meat on bone, meat_on_bone" + case .poultryLeg: return "poultry_leg, poultry leg, poultryleg" + case .cutOfMeat: return "cut of meat, cutofmeat, cut_of_meat" + case .bacon: return "bacon" + case .hamburger: return "hamburger" + case .fries: return "french fries, fries" + case .pizza: return "slice of pizza, pizza" + case .hotdog: return "hotdog, hot dog" + case .sandwich: return "sandwich" + case .taco: return "taco" + case .burrito: return "burrito" + case .tamale: return "tamale" + case .stuffedFlatbread: return "stuffed_flatbread, stuffed flatbread, stuffedflatbread" + case .falafel: return "falafel" + case .egg: return "egg" + case .friedEgg: return "friedegg, fried_egg, cooking" + case .shallowPanOfFood: return "shallow pan of food, shallow_pan_of_food, shallowpanoffood" + case .stew: return "pot of food, stew" + case .fondue: return "fondue" + case .bowlWithSpoon: return "bowlwithspoon, bowl_with_spoon, bowl with spoon" + case .greenSalad: return "greensalad, green salad, green_salad" + case .popcorn: return "popcorn" + case .butter: return "butter" + case .salt: return "salt shaker, salt" + case .cannedFood: return "canned_food, canned food, cannedfood" + case .bento: return "bento, bento box" + case .riceCracker: return "rice_cracker, ricecracker, rice cracker" + case .riceBall: return "rice ball, riceball, rice_ball" + case .rice: return "rice, cooked rice" + case .curry: return "curry, curry and rice" + case .ramen: return "steaming bowl, ramen" + case .spaghetti: return "spaghetti" + case .sweetPotato: return "sweet_potato, roasted sweet potato, sweetpotato" + case .oden: return "oden" + case .sushi: return "sushi" + case .friedShrimp: return "fried_shrimp, friedshrimp, fried shrimp" + case .fishCake: return "fish_cake, fish cake with swirl design, fishcake" + case .moonCake: return "mooncake, moon_cake, moon cake" + case .dango: return "dango" + case .dumpling: return "dumpling" + case .fortuneCookie: return "fortune_cookie, fortune cookie, fortunecookie" + case .takeoutBox: return "takeoutbox, takeout_box, takeout box" + case .crab: return "crab" + case .lobster: return "lobster" + case .shrimp: return "shrimp" + case .squid: return "squid" + case .oyster: return "oyster" + case .icecream: return "icecream, soft ice cream" + case .shavedIce: return "shaved ice, shavedice, shaved_ice" + case .iceCream: return "ice cream, icecream, ice_cream" + case .doughnut: return "doughnut" + case .cookie: return "cookie" + case .birthday: return "birthday cake, birthday" + case .cake: return "cake, shortcake" + case .cupcake: return "cupcake" + case .pie: return "pie" + case .chocolateBar: return "chocolate bar, chocolate_bar, chocolatebar" + case .candy: return "candy" + case .lollipop: return "lollipop" + case .custard: return "custard" + case .honeyPot: return "honey pot, honeypot, honey_pot" + case .babyBottle: return "baby_bottle, baby bottle, babybottle" + case .glassOfMilk: return "glass of milk, glassofmilk, glass_of_milk" + case .coffee: return "hot beverage, coffee" + case .teapot: return "teapot" + case .tea: return "tea, teacup without handle" + case .sake: return "sake bottle and cup, sake" + case .champagne: return "champagne, bottle with popping cork" + case .wineGlass: return "wine_glass, wine glass, wineglass" + case .cocktail: return "cocktail, cocktail glass" + case .tropicalDrink: return "tropicaldrink, tropical drink, tropical_drink" + case .beer: return "beer, beer mug" + case .beers: return "beers, clinking beer mugs" + case .clinkingGlasses: return "clinkingglasses, clinking_glasses, clinking glasses" + case .tumblerGlass: return "tumbler glass, tumblerglass, tumbler_glass" + case .pouringLiquid: return "pouring_liquid, pouring liquid, pouringliquid" + case .cupWithStraw: return "cup with straw, cupwithstraw, cup_with_straw" + case .bubbleTea: return "bubbletea, bubble_tea, bubble tea" + case .beverageBox: return "beverage box, beverage_box, beveragebox" + case .mateDrink: return "mate_drink, mate drink, matedrink" + case .iceCube: return "ice_cube, ice cube, icecube" + case .chopsticks: return "chopsticks" + case .knifeForkPlate: return "fork and knife with plate, knifeforkplate, knife_fork_plate" + case .forkAndKnife: return "forkandknife, fork and knife, fork_and_knife" + case .spoon: return "spoon" + case .hocho: return "hocho, knife" + case .jar: return "jar" + case .amphora: return "amphora" + case .earthAfrica: return "earth globe europe-africa, earth_africa, earthafrica" + case .earthAmericas: return "earth globe americas, earthamericas, earth_americas" + case .earthAsia: return "earthasia, earth_asia, earth globe asia-australia" + case .globeWithMeridians: return "globewithmeridians, globe_with_meridians, globe with meridians" + case .worldMap: return "world_map, world map, worldmap" + case .japan: return "japan, silhouette of japan" + case .compass: return "compass" + case .snowCappedMountain: return "snow_capped_mountain, snow-capped mountain, snowcappedmountain" + case .mountain: return "mountain" + case .volcano: return "volcano" + case .mountFuji: return "mount_fuji, mount fuji, mountfuji" + case .camping: return "camping" + case .beachWithUmbrella: return "beach with umbrella, beach_with_umbrella, beachwithumbrella" + case .desert: return "desert" + case .desertIsland: return "desert_island, desert island, desertisland" + case .nationalPark: return "nationalpark, national_park, national park" + case .stadium: return "stadium" + case .classicalBuilding: return "classical_building, classical building, classicalbuilding" + case .buildingConstruction: return "building_construction, buildingconstruction, building construction" + case .bricks: return "brick, bricks" + case .rock: return "rock" + case .wood: return "wood" + case .hut: return "hut" + case .houseBuildings: return "housebuildings, house_buildings, houses" + case .derelictHouseBuilding: return "derelict_house_building, derelict house, derelicthousebuilding" + case .house: return "house, house building" + case .houseWithGarden: return "house with garden, house_with_garden, housewithgarden" + case .office: return "office building, office" + case .postOffice: return "post_office, japanese post office, postoffice" + case .europeanPostOffice: return "european post office, european_post_office, europeanpostoffice" + case .hospital: return "hospital" + case .bank: return "bank" + case .hotel: return "hotel" + case .loveHotel: return "love_hotel, love hotel, lovehotel" + case .convenienceStore: return "convenience store, conveniencestore, convenience_store" + case .school: return "school" + case .departmentStore: return "department_store, department store, departmentstore" + case .factory: return "factory" + case .japaneseCastle: return "japanese_castle, japanese castle, japanesecastle" + case .europeanCastle: return "europeancastle, european_castle, european castle" + case .wedding: return "wedding" + case .tokyoTower: return "tokyo tower, tokyotower, tokyo_tower" + case .statueOfLiberty: return "statue of liberty, statueofliberty, statue_of_liberty" + case .church: return "church" + case .mosque: return "mosque" + case .hinduTemple: return "hindu temple, hindu_temple, hindutemple" + case .synagogue: return "synagogue" + case .shintoShrine: return "shinto shrine, shintoshrine, shinto_shrine" + case .kaaba: return "kaaba" + case .fountain: return "fountain" + case .tent: return "tent" + case .foggy: return "foggy" + case .nightWithStars: return "night with stars, nightwithstars, night_with_stars" + case .cityscape: return "cityscape" + case .sunriseOverMountains: return "sunrise_over_mountains, sunrise over mountains, sunriseovermountains" + case .sunrise: return "sunrise" + case .citySunset: return "cityscape at dusk, city_sunset, citysunset" + case .citySunrise: return "city_sunrise, sunset over buildings, citysunrise" + case .bridgeAtNight: return "bridge at night, bridge_at_night, bridgeatnight" + case .hotsprings: return "hotsprings, hot springs" + case .carouselHorse: return "carousel horse, carousel_horse, carouselhorse" + case .playgroundSlide: return "playground_slide, playground slide, playgroundslide" + case .ferrisWheel: return "ferris_wheel, ferriswheel, ferris wheel" + case .rollerCoaster: return "roller_coaster, rollercoaster, roller coaster" + case .barber: return "barber pole, barber" + case .circusTent: return "circus tent, circustent, circus_tent" + case .steamLocomotive: return "steam_locomotive, steam locomotive, steamlocomotive" + case .railwayCar: return "railwaycar, railway_car, railway car" + case .bullettrainSide: return "high-speed train, bullettrain_side, bullettrainside" + case .bullettrainFront: return "high-speed train with bullet nose, bullettrain_front, bullettrainfront" + case .train2: return "train2, train" + case .metro: return "metro" + case .lightRail: return "light rail, light_rail, lightrail" + case .station: return "station" + case .tram: return "tram" + case .monorail: return "monorail" + case .mountainRailway: return "mountain railway, mountainrailway, mountain_railway" + case .train: return "train, tram car" + case .bus: return "bus" + case .oncomingBus: return "oncoming bus, oncomingbus, oncoming_bus" + case .trolleybus: return "trolleybus" + case .minibus: return "minibus" + case .ambulance: return "ambulance" + case .fireEngine: return "fire_engine, fire engine, fireengine" + case .policeCar: return "police_car, policecar, police car" + case .oncomingPoliceCar: return "oncoming_police_car, oncomingpolicecar, oncoming police car" + case .taxi: return "taxi" + case .oncomingTaxi: return "oncoming_taxi, oncoming taxi, oncomingtaxi" + case .car: return "car, red_car, automobile" + case .oncomingAutomobile: return "oncoming automobile, oncoming_automobile, oncomingautomobile" + case .blueCar: return "bluecar, blue_car, recreational vehicle" + case .pickupTruck: return "pickup_truck, pickup truck, pickuptruck" + case .truck: return "delivery truck, truck" + case .articulatedLorry: return "articulated_lorry, articulated lorry, articulatedlorry" + case .tractor: return "tractor" + case .racingCar: return "racing car, racingcar, racing_car" + case .racingMotorcycle: return "racing_motorcycle, motorcycle, racingmotorcycle" + case .motorScooter: return "motor scooter, motor_scooter, motorscooter" + case .manualWheelchair: return "manual_wheelchair, manualwheelchair, manual wheelchair" + case .motorizedWheelchair: return "motorized_wheelchair, motorized wheelchair, motorizedwheelchair" + case .autoRickshaw: return "auto rickshaw, auto_rickshaw, autorickshaw" + case .bike: return "bicycle, bike" + case .scooter: return "scooter" + case .skateboard: return "skateboard" + case .rollerSkate: return "roller skate, rollerskate, roller_skate" + case .busstop: return "bus stop, busstop" + case .motorway: return "motorway" + case .railwayTrack: return "railwaytrack, railway track, railway_track" + case .oilDrum: return "oil_drum, oil drum, oildrum" + case .fuelpump: return "fuelpump, fuel pump" + case .wheel: return "wheel" + case .rotatingLight: return "police cars revolving light, rotating_light, rotatinglight" + case .trafficLight: return "horizontal traffic light, trafficlight, traffic_light" + case .verticalTrafficLight: return "verticaltrafficlight, vertical traffic light, vertical_traffic_light" + case .octagonalSign: return "octagonal_sign, octagonalsign, octagonal sign" + case .construction: return "construction sign, construction" + case .anchor: return "anchor" + case .ringBuoy: return "ring_buoy, ringbuoy, ring buoy" + case .boat: return "sailboat, boat" + case .canoe: return "canoe" + case .speedboat: return "speedboat" + case .passengerShip: return "passenger_ship, passenger ship, passengership" + case .ferry: return "ferry" + case .motorBoat: return "motorboat, motor_boat, motor boat" + case .ship: return "ship" + case .airplane: return "airplane" + case .smallAirplane: return "small_airplane, smallairplane, small airplane" + case .airplaneDeparture: return "airplane departure, airplane_departure, airplanedeparture" + case .airplaneArriving: return "airplane arriving, airplane_arriving, airplanearriving" + case .parachute: return "parachute" + case .seat: return "seat" + case .helicopter: return "helicopter" + case .suspensionRailway: return "suspension_railway, suspension railway, suspensionrailway" + case .mountainCableway: return "mountain_cableway, mountain cableway, mountaincableway" + case .aerialTramway: return "aerial tramway, aerialtramway, aerial_tramway" + case .satellite: return "satellite" + case .rocket: return "rocket" + case .flyingSaucer: return "flying saucer, flyingsaucer, flying_saucer" + case .bellhopBell: return "bellhop_bell, bellhop bell, bellhopbell" + case .luggage: return "luggage" + case .hourglass: return "hourglass" + case .hourglassFlowingSand: return "hourglass with flowing sand, hourglass_flowing_sand, hourglassflowingsand" + case .watch: return "watch" + case .alarmClock: return "alarm_clock, alarmclock, alarm clock" + case .stopwatch: return "stopwatch" + case .timerClock: return "timer_clock, timerclock, timer clock" + case .mantelpieceClock: return "mantelpiece_clock, mantelpiece clock, mantelpiececlock" + case .clock12: return "clock12, clock face twelve oclock" + case .clock1230: return "clock face twelve-thirty, clock1230" + case .clock1: return "clock face one oclock, clock1" + case .clock130: return "clock130, clock face one-thirty" + case .clock2: return "clock face two oclock, clock2" + case .clock230: return "clock230, clock face two-thirty" + case .clock3: return "clock3, clock face three oclock" + case .clock330: return "clock330, clock face three-thirty" + case .clock4: return "clock face four oclock, clock4" + case .clock430: return "clock430, clock face four-thirty" + case .clock5: return "clock face five oclock, clock5" + case .clock530: return "clock530, clock face five-thirty" + case .clock6: return "clock6, clock face six oclock" + case .clock630: return "clock face six-thirty, clock630" + case .clock7: return "clock face seven oclock, clock7" + case .clock730: return "clock face seven-thirty, clock730" + case .clock8: return "clock face eight oclock, clock8" + case .clock830: return "clock face eight-thirty, clock830" + case .clock9: return "clock face nine oclock, clock9" + case .clock930: return "clock930, clock face nine-thirty" + case .clock10: return "clock10, clock face ten oclock" + case .clock1030: return "clock1030, clock face ten-thirty" + case .clock11: return "clock face eleven oclock, clock11" + case .clock1130: return "clock1130, clock face eleven-thirty" + case .newMoon: return "new moon symbol, newmoon, new_moon" + case .waxingCrescentMoon: return "waxing crescent moon symbol, waxing_crescent_moon, waxingcrescentmoon" + case .firstQuarterMoon: return "firstquartermoon, first_quarter_moon, first quarter moon symbol" + case .moon: return "waxing gibbous moon symbol, waxing_gibbous_moon, moon" + case .fullMoon: return "full_moon, full moon symbol, fullmoon" + case .waningGibbousMoon: return "waning_gibbous_moon, waning gibbous moon symbol, waninggibbousmoon" + case .lastQuarterMoon: return "lastquartermoon, last_quarter_moon, last quarter moon symbol" + case .waningCrescentMoon: return "waning_crescent_moon, waning crescent moon symbol, waningcrescentmoon" + case .crescentMoon: return "crescent_moon, crescent moon, crescentmoon" + case .newMoonWithFace: return "newmoonwithface, new_moon_with_face, new moon with face" + case .firstQuarterMoonWithFace: return "first quarter moon with face, first_quarter_moon_with_face, firstquartermoonwithface" + case .lastQuarterMoonWithFace: return "lastquartermoonwithface, last_quarter_moon_with_face, last quarter moon with face" + case .thermometer: return "thermometer" + case .sunny: return "black sun with rays, sunny" + case .fullMoonWithFace: return "full_moon_with_face, full moon with face, fullmoonwithface" + case .sunWithFace: return "sun_with_face, sunwithface, sun with face" + case .ringedPlanet: return "ringed_planet, ringed planet, ringedplanet" + case .star: return "star, white medium star" + case .star2: return "star2, glowing star" + case .stars: return "shooting star, stars" + case .milkyWay: return "milky way, milky_way, milkyway" + case .cloud: return "cloud" + case .partlySunny: return "partly_sunny, sun behind cloud, partlysunny" + case .thunderCloudAndRain: return "thunder_cloud_and_rain, cloud with lightning and rain, thundercloudandrain" + case .mostlySunny: return "sun_small_cloud, mostlysunny, sun behind small cloud, mostly_sunny" + case .barelySunny: return "sun_behind_cloud, barely_sunny, sun behind large cloud, barelysunny" + case .partlySunnyRain: return "sun behind rain cloud, partly_sunny_rain, partlysunnyrain, sun_behind_rain_cloud" + case .rainCloud: return "cloud with rain, raincloud, rain_cloud" + case .snowCloud: return "snow_cloud, snowcloud, cloud with snow" + case .lightning: return "cloud with lightning, lightning, lightning_cloud" + case .tornado: return "tornado, tornado_cloud" + case .fog: return "fog" + case .windBlowingFace: return "wind face, wind_blowing_face, windblowingface" + case .cyclone: return "cyclone" + case .rainbow: return "rainbow" + case .closedUmbrella: return "closed_umbrella, closedumbrella, closed umbrella" + case .umbrella: return "umbrella" + case .umbrellaWithRainDrops: return "umbrella with rain drops, umbrellawithraindrops, umbrella_with_rain_drops" + case .umbrellaOnGround: return "umbrella on ground, umbrella_on_ground, umbrellaonground" + case .zap: return "high voltage sign, zap" + case .snowflake: return "snowflake" + case .snowman: return "snowman" + case .snowmanWithoutSnow: return "snowman without snow, snowman_without_snow, snowmanwithoutsnow" + case .comet: return "comet" + case .fire: return "fire" + case .droplet: return "droplet" + case .ocean: return "ocean, water wave" + case .jackOLantern: return "jack-o-lantern, jack_o_lantern, jackolantern" + case .christmasTree: return "christmastree, christmas_tree, christmas tree" + case .fireworks: return "fireworks" + case .sparkler: return "sparkler, firework sparkler" + case .firecracker: return "firecracker" + case .sparkles: return "sparkles" + case .balloon: return "balloon" + case .tada: return "tada, party popper" + case .confettiBall: return "confetti_ball, confettiball, confetti ball" + case .tanabataTree: return "tanabatatree, tanabata tree, tanabata_tree" + case .bamboo: return "bamboo, pine decoration" + case .dolls: return "dolls, japanese dolls" + case .flags: return "flags, carp streamer" + case .windChime: return "windchime, wind_chime, wind chime" + case .riceScene: return "moon viewing ceremony, rice_scene, ricescene" + case .redEnvelope: return "red gift envelope, redenvelope, red_envelope" + case .ribbon: return "ribbon" + case .gift: return "gift, wrapped present" + case .reminderRibbon: return "reminder ribbon, reminderribbon, reminder_ribbon" + case .admissionTickets: return "admission_tickets, admission tickets, admissiontickets" + case .ticket: return "ticket" + case .medal: return "medal, military medal" + case .trophy: return "trophy" + case .sportsMedal: return "sportsmedal, sports medal, sports_medal" + case .firstPlaceMedal: return "first place medal, firstplacemedal, first_place_medal" + case .secondPlaceMedal: return "secondplacemedal, second_place_medal, second place medal" + case .thirdPlaceMedal: return "third_place_medal, thirdplacemedal, third place medal" + case .soccer: return "soccer ball, soccer" + case .baseball: return "baseball" + case .softball: return "softball" + case .basketball: return "basketball and hoop, basketball" + case .volleyball: return "volleyball" + case .football: return "football, american football" + case .rugbyFootball: return "rugby_football, rugby football, rugbyfootball" + case .tennis: return "tennis, tennis racquet and ball" + case .flyingDisc: return "flying_disc, flyingdisc, flying disc" + case .bowling: return "bowling" + case .cricketBatAndBall: return "cricket_bat_and_ball, cricketbatandball, cricket bat and ball" + case .fieldHockeyStickAndBall: return "field_hockey_stick_and_ball, field hockey stick and ball, fieldhockeystickandball" + case .iceHockeyStickAndPuck: return "ice_hockey_stick_and_puck, ice hockey stick and puck, icehockeystickandpuck" + case .lacrosse: return "lacrosse stick and ball, lacrosse" + case .tableTennisPaddleAndBall: return "table tennis paddle and ball, table_tennis_paddle_and_ball, tabletennispaddleandball" + case .badmintonRacquetAndShuttlecock: return "badminton_racquet_and_shuttlecock, badminton racquet and shuttlecock, badmintonracquetandshuttlecock" + case .boxingGlove: return "boxing_glove, boxing glove, boxingglove" + case .martialArtsUniform: return "martial_arts_uniform, martial arts uniform, martialartsuniform" + case .goalNet: return "goalnet, goal net, goal_net" + case .golf: return "golf, flag in hole" + case .iceSkate: return "ice skate, iceskate, ice_skate" + case .fishingPoleAndFish: return "fishing pole and fish, fishingpoleandfish, fishing_pole_and_fish" + case .divingMask: return "divingmask, diving_mask, diving mask" + case .runningShirtWithSash: return "running shirt with sash, running_shirt_with_sash, runningshirtwithsash" + case .ski: return "ski, ski and ski boot" + case .sled: return "sled" + case .curlingStone: return "curling_stone, curling stone, curlingstone" + case .dart: return "dart, direct hit" + case .yoYo: return "yo-yo, yoyo" + case .kite: return "kite" + case .eightBall: return "8ball, billiards, eightball" + case .crystalBall: return "crystal ball, crystalball, crystal_ball" + case .magicWand: return "magic wand, magicwand, magic_wand" + case .nazarAmulet: return "nazar amulet, nazaramulet, nazar_amulet" + case .hamsa: return "hamsa" + case .videoGame: return "video_game, video game, videogame" + case .joystick: return "joystick" + case .slotMachine: return "slotmachine, slot_machine, slot machine" + case .gameDie: return "gamedie, game die, game_die" + case .jigsaw: return "jigsaw, jigsaw puzzle piece" + case .teddyBear: return "teddy_bear, teddy bear, teddybear" + case .pinata: return "pinata" + case .mirrorBall: return "mirrorball, mirror ball, mirror_ball" + case .nestingDolls: return "nesting dolls, nestingdolls, nesting_dolls" + case .spades: return "black spade suit, spades" + case .hearts: return "black heart suit, hearts" + case .diamonds: return "diamonds, black diamond suit" + case .clubs: return "clubs, black club suit" + case .chessPawn: return "chess_pawn, chess pawn, chesspawn" + case .blackJoker: return "black_joker, blackjoker, playing card black joker" + case .mahjong: return "mahjong, mahjong tile red dragon" + case .flowerPlayingCards: return "flower playing cards, flowerplayingcards, flower_playing_cards" + case .performingArts: return "performingarts, performing_arts, performing arts" + case .frameWithPicture: return "framed picture, framewithpicture, frame_with_picture" + case .art: return "art, artist palette" + case .thread: return "thread, spool of thread" + case .sewingNeedle: return "sewing needle, sewingneedle, sewing_needle" + case .yarn: return "ball of yarn, yarn" + case .knot: return "knot" + case .eyeglasses: return "eyeglasses" + case .darkSunglasses: return "sunglasses, darksunglasses, dark_sunglasses" + case .goggles: return "goggles" + case .labCoat: return "lab_coat, lab coat, labcoat" + case .safetyVest: return "safety_vest, safetyvest, safety vest" + case .necktie: return "necktie" + case .shirt: return "t-shirt, shirt, tshirt" + case .jeans: return "jeans" + case .scarf: return "scarf" + case .gloves: return "gloves" + case .coat: return "coat" + case .socks: return "socks" + case .dress: return "dress" + case .kimono: return "kimono" + case .sari: return "sari" + case .onePieceSwimsuit: return "one-piece swimsuit, onepieceswimsuit, one-piece_swimsuit" + case .briefs: return "briefs" + case .shorts: return "shorts" + case .bikini: return "bikini" + case .womansClothes: return "womans_clothes, womansclothes, womans clothes" + case .purse: return "purse" + case .handbag: return "handbag" + case .pouch: return "pouch" + case .shoppingBags: return "shopping bags, shoppingbags, shopping_bags" + case .schoolSatchel: return "school satchel, schoolsatchel, school_satchel" + case .thongSandal: return "thong_sandal, thongsandal, thong sandal" + case .mansShoe: return "mans_shoe, shoe, mans shoe, mansshoe" + case .athleticShoe: return "athletic_shoe, athletic shoe, athleticshoe" + case .hikingBoot: return "hikingboot, hiking boot, hiking_boot" + case .womansFlatShoe: return "flat shoe, womansflatshoe, womans_flat_shoe" + case .highHeel: return "high-heeled shoe, high_heel, highheel" + case .sandal: return "sandal, womans sandal" + case .balletShoes: return "balletshoes, ballet_shoes, ballet shoes" + case .boot: return "boot, womans boots" + case .crown: return "crown" + case .womansHat: return "womans_hat, womanshat, womans hat" + case .tophat: return "tophat, top hat" + case .mortarBoard: return "mortarboard, mortar_board, graduation cap" + case .billedCap: return "billed_cap, billed cap, billedcap" + case .militaryHelmet: return "militaryhelmet, military helmet, military_helmet" + case .helmetWithWhiteCross: return "helmet_with_white_cross, helmetwithwhitecross, rescue worker’s helmet" + case .prayerBeads: return "prayer beads, prayer_beads, prayerbeads" + case .lipstick: return "lipstick" + case .ring: return "ring" + case .gem: return "gem, gem stone" + case .mute: return "mute, speaker with cancellation stroke" + case .speaker: return "speaker" + case .sound: return "sound, speaker with one sound wave" + case .loudSound: return "loud_sound, speaker with three sound waves, loudsound" + case .loudspeaker: return "public address loudspeaker, loudspeaker" + case .mega: return "mega, cheering megaphone" + case .postalHorn: return "postal horn, postal_horn, postalhorn" + case .bell: return "bell" + case .noBell: return "nobell, no_bell, bell with cancellation stroke" + case .musicalScore: return "musical_score, musicalscore, musical score" + case .musicalNote: return "musical_note, musical note, musicalnote" + case .notes: return "multiple musical notes, notes" + case .studioMicrophone: return "studio microphone, studio_microphone, studiomicrophone" + case .levelSlider: return "levelslider, level slider, level_slider" + case .controlKnobs: return "control_knobs, control knobs, controlknobs" + case .microphone: return "microphone" + case .headphones: return "headphones, headphone" + case .radio: return "radio" + case .saxophone: return "saxophone" + case .accordion: return "accordion" + case .guitar: return "guitar" + case .musicalKeyboard: return "musicalkeyboard, musical keyboard, musical_keyboard" + case .trumpet: return "trumpet" + case .violin: return "violin" + case .banjo: return "banjo" + case .drumWithDrumsticks: return "drum_with_drumsticks, drum with drumsticks, drumwithdrumsticks" + case .longDrum: return "long drum, long_drum, longdrum" + case .iphone: return "iphone, mobile phone" + case .calling: return "calling, mobile phone with rightwards arrow at left" + case .phone: return "black telephone, phone, telephone" + case .telephoneReceiver: return "telephone receiver, telephonereceiver, telephone_receiver" + case .pager: return "pager" + case .fax: return "fax machine, fax" + case .battery: return "battery" + case .lowBattery: return "lowbattery, low_battery, low battery" + case .electricPlug: return "electricplug, electric plug, electric_plug" + case .computer: return "personal computer, computer" + case .desktopComputer: return "desktop_computer, desktop computer, desktopcomputer" + case .printer: return "printer" + case .keyboard: return "keyboard" + case .threeButtonMouse: return "computer mouse, threebuttonmouse, three_button_mouse" + case .trackball: return "trackball" + case .minidisc: return "minidisc" + case .floppyDisk: return "floppydisk, floppy_disk, floppy disk" + case .cd: return "optical disc, cd" + case .dvd: return "dvd" + case .abacus: return "abacus" + case .movieCamera: return "movie camera, moviecamera, movie_camera" + case .filmFrames: return "filmframes, film frames, film_frames" + case .filmProjector: return "film_projector, filmprojector, film projector" + case .clapper: return "clapper, clapper board" + case .tv: return "television, tv" + case .camera: return "camera" + case .cameraWithFlash: return "camera with flash, camerawithflash, camera_with_flash" + case .videoCamera: return "video_camera, video camera, videocamera" + case .vhs: return "vhs, videocassette" + case .mag: return "mag, left-pointing magnifying glass" + case .magRight: return "mag_right, magright, right-pointing magnifying glass" + case .candle: return "candle" + case .bulb: return "electric light bulb, bulb" + case .flashlight: return "electric torch, flashlight" + case .izakayaLantern: return "izakaya_lantern, izakaya lantern, lantern, izakayalantern" + case .diyaLamp: return "diyalamp, diya_lamp, diya lamp" + case .notebookWithDecorativeCover: return "notebook_with_decorative_cover, notebook with decorative cover, notebookwithdecorativecover" + case .closedBook: return "closed_book, closed book, closedbook" + case .book: return "book, open_book, open book" + case .greenBook: return "greenbook, green book, green_book" + case .blueBook: return "blue_book, bluebook, blue book" + case .orangeBook: return "orange book, orangebook, orange_book" + case .books: return "books" + case .notebook: return "notebook" + case .ledger: return "ledger" + case .pageWithCurl: return "page with curl, page_with_curl, pagewithcurl" + case .scroll: return "scroll" + case .pageFacingUp: return "pagefacingup, page_facing_up, page facing up" + case .newspaper: return "newspaper" + case .rolledUpNewspaper: return "rolledupnewspaper, rolled_up_newspaper, rolled-up newspaper" + case .bookmarkTabs: return "bookmarktabs, bookmark_tabs, bookmark tabs" + case .bookmark: return "bookmark" + case .label: return "label" + case .moneybag: return "money bag, moneybag" + case .coin: return "coin" + case .yen: return "banknote with yen sign, yen" + case .dollar: return "dollar, banknote with dollar sign" + case .euro: return "banknote with euro sign, euro" + case .pound: return "pound, banknote with pound sign" + case .moneyWithWings: return "money_with_wings, money with wings, moneywithwings" + case .creditCard: return "creditcard, credit card, credit_card" + case .receipt: return "receipt" + case .chart: return "chart, chart with upwards trend and yen sign" + case .email: return "email, envelope" + case .eMail: return "email, e-mail, e-mail symbol" + case .incomingEnvelope: return "incomingenvelope, incoming_envelope, incoming envelope" + case .envelopeWithArrow: return "envelope with downwards arrow above, envelopewitharrow, envelope_with_arrow" + case .outboxTray: return "outbox tray, outboxtray, outbox_tray" + case .inboxTray: return "inboxtray, inbox_tray, inbox tray" + case .package: return "package" + case .mailbox: return "closed mailbox with raised flag, mailbox" + case .mailboxClosed: return "mailbox_closed, closed mailbox with lowered flag, mailboxclosed" + case .mailboxWithMail: return "mailboxwithmail, mailbox_with_mail, open mailbox with raised flag" + case .mailboxWithNoMail: return "open mailbox with lowered flag, mailboxwithnomail, mailbox_with_no_mail" + case .postbox: return "postbox" + case .ballotBoxWithBallot: return "ballotboxwithballot, ballot box with ballot, ballot_box_with_ballot" + case .pencil2: return "pencil2, pencil" + case .blackNib: return "black nib, black_nib, blacknib" + case .lowerLeftFountainPen: return "lowerleftfountainpen, lower_left_fountain_pen, fountain pen" + case .lowerLeftBallpointPen: return "pen, lowerleftballpointpen, lower_left_ballpoint_pen" + case .lowerLeftPaintbrush: return "lowerleftpaintbrush, paintbrush, lower_left_paintbrush" + case .lowerLeftCrayon: return "crayon, lowerleftcrayon, lower_left_crayon" + case .memo: return "memo, pencil" + case .briefcase: return "briefcase" + case .fileFolder: return "filefolder, file folder, file_folder" + case .openFileFolder: return "openfilefolder, open file folder, open_file_folder" + case .cardIndexDividers: return "card index dividers, cardindexdividers, card_index_dividers" + case .date: return "calendar, date" + case .calendar: return "calendar, tear-off calendar" + case .spiralNotePad: return "spiralnotepad, spiral notepad, spiral_note_pad" + case .spiralCalendarPad: return "spiralcalendarpad, spiral calendar, spiral_calendar_pad" + case .cardIndex: return "card index, cardindex, card_index" + case .chartWithUpwardsTrend: return "chartwithupwardstrend, chart with upwards trend, chart_with_upwards_trend" + case .chartWithDownwardsTrend: return "chartwithdownwardstrend, chart with downwards trend, chart_with_downwards_trend" + case .barChart: return "barchart, bar chart, bar_chart" + case .clipboard: return "clipboard" + case .pushpin: return "pushpin" + case .roundPushpin: return "round pushpin, round_pushpin, roundpushpin" + case .paperclip: return "paperclip" + case .linkedPaperclips: return "linked paperclips, linked_paperclips, linkedpaperclips" + case .straightRuler: return "straightruler, straight ruler, straight_ruler" + case .triangularRuler: return "triangular ruler, triangularruler, triangular_ruler" + case .scissors: return "black scissors, scissors" + case .cardFileBox: return "card file box, card_file_box, cardfilebox" + case .fileCabinet: return "file_cabinet, filecabinet, file cabinet" + case .wastebasket: return "wastebasket" + case .lock: return "lock" + case .unlock: return "open lock, unlock" + case .lockWithInkPen: return "lock_with_ink_pen, lock with ink pen, lockwithinkpen" + case .closedLockWithKey: return "closedlockwithkey, closed_lock_with_key, closed lock with key" + case .key: return "key" + case .oldKey: return "oldkey, old key, old_key" + case .hammer: return "hammer" + case .axe: return "axe" + case .pick: return "pick" + case .hammerAndPick: return "hammerandpick, hammer and pick, hammer_and_pick" + case .hammerAndWrench: return "hammerandwrench, hammer_and_wrench, hammer and wrench" + case .daggerKnife: return "daggerknife, dagger_knife, dagger" + case .crossedSwords: return "crossedswords, crossed_swords, crossed swords" + case .gun: return "gun, pistol" + case .boomerang: return "boomerang" + case .bowAndArrow: return "bow_and_arrow, bowandarrow, bow and arrow" + case .shield: return "shield" + case .carpentrySaw: return "carpentry_saw, carpentry saw, carpentrysaw" + case .wrench: return "wrench" + case .screwdriver: return "screwdriver" + case .nutAndBolt: return "nut_and_bolt, nut and bolt, nutandbolt" + case .gear: return "gear" + case .compression: return "clamp, compression" + case .scales: return "balance scale, scales" + case .probingCane: return "probing cane, probing_cane, probingcane" + case .link: return "link, link symbol" + case .chains: return "chains" + case .hook: return "hook" + case .toolbox: return "toolbox" + case .magnet: return "magnet" + case .ladder: return "ladder" + case .alembic: return "alembic" + case .testTube: return "test tube, testtube, test_tube" + case .petriDish: return "petri dish, petri_dish, petridish" + case .dna: return "dna, dna double helix" + case .microscope: return "microscope" + case .telescope: return "telescope" + case .satelliteAntenna: return "satelliteantenna, satellite_antenna, satellite antenna" + case .syringe: return "syringe" + case .dropOfBlood: return "drop of blood, dropofblood, drop_of_blood" + case .pill: return "pill" + case .adhesiveBandage: return "adhesive bandage, adhesive_bandage, adhesivebandage" + case .crutch: return "crutch" + case .stethoscope: return "stethoscope" + case .xRay: return "x-ray, xray" + case .door: return "door" + case .elevator: return "elevator" + case .mirror: return "mirror" + case .window: return "window" + case .bed: return "bed" + case .couchAndLamp: return "couch_and_lamp, couchandlamp, couch and lamp" + case .chair: return "chair" + case .toilet: return "toilet" + case .plunger: return "plunger" + case .shower: return "shower" + case .bathtub: return "bathtub" + case .mouseTrap: return "mousetrap, mouse_trap, mouse trap" + case .razor: return "razor" + case .lotionBottle: return "lotionbottle, lotion_bottle, lotion bottle" + case .safetyPin: return "safety pin, safetypin, safety_pin" + case .broom: return "broom" + case .basket: return "basket" + case .rollOfPaper: return "roll_of_paper, rollofpaper, roll of paper" + case .bucket: return "bucket" + case .soap: return "soap, bar of soap" + case .bubbles: return "bubbles" + case .toothbrush: return "toothbrush" + case .sponge: return "sponge" + case .fireExtinguisher: return "fire extinguisher, fire_extinguisher, fireextinguisher" + case .shoppingTrolley: return "shopping_trolley, shopping trolley, shoppingtrolley" + case .smoking: return "smoking, smoking symbol" + case .coffin: return "coffin" + case .headstone: return "headstone" + case .funeralUrn: return "funeralurn, funeral urn, funeral_urn" + case .moyai: return "moyai" + case .placard: return "placard" + case .identificationCard: return "identification_card, identification card, identificationcard" + case .atm: return "atm, automated teller machine" + case .putLitterInItsPlace: return "putlitterinitsplace, put_litter_in_its_place, put litter in its place symbol" + case .potableWater: return "potablewater, potable water symbol, potable_water" + case .wheelchair: return "wheelchair symbol, wheelchair" + case .mens: return "mens, mens symbol" + case .womens: return "womens symbol, womens" + case .restroom: return "restroom" + case .babySymbol: return "baby_symbol, babysymbol, baby symbol" + case .wc: return "wc, water closet" + case .passportControl: return "passport_control, passport control, passportcontrol" + case .customs: return "customs" + case .baggageClaim: return "baggageclaim, baggage claim, baggage_claim" + case .leftLuggage: return "left luggage, leftluggage, left_luggage" + case .warning: return "warning, warning sign" + case .childrenCrossing: return "children crossing, childrencrossing, children_crossing" + case .noEntry: return "no entry, no_entry, noentry" + case .noEntrySign: return "no entry sign, noentrysign, no_entry_sign" + case .noBicycles: return "no bicycles, no_bicycles, nobicycles" + case .noSmoking: return "nosmoking, no_smoking, no smoking symbol" + case .doNotLitter: return "donotlitter, do not litter symbol, do_not_litter" + case .nonPotableWater: return "nonpotablewater, non-potable_water, non-potable water symbol" + case .noPedestrians: return "nopedestrians, no pedestrians, no_pedestrians" + case .noMobilePhones: return "no mobile phones, nomobilephones, no_mobile_phones" + case .underage: return "underage, no one under eighteen symbol" + case .radioactiveSign: return "radioactive, radioactivesign, radioactive_sign" + case .biohazardSign: return "biohazard, biohazardsign, biohazard_sign" + case .arrowUp: return "arrowup, arrow_up, upwards black arrow" + case .arrowUpperRight: return "north east arrow, arrow_upper_right, arrowupperright" + case .arrowRight: return "arrowright, black rightwards arrow, arrow_right" + case .arrowLowerRight: return "arrowlowerright, arrow_lower_right, south east arrow" + case .arrowDown: return "downwards black arrow, arrowdown, arrow_down" + case .arrowLowerLeft: return "arrow_lower_left, south west arrow, arrowlowerleft" + case .arrowLeft: return "arrow_left, leftwards black arrow, arrowleft" + case .arrowUpperLeft: return "arrow_upper_left, north west arrow, arrowupperleft" + case .arrowUpDown: return "arrowupdown, arrow_up_down, up down arrow" + case .leftRightArrow: return "left_right_arrow, left right arrow, leftrightarrow" + case .leftwardsArrowWithHook: return "leftwards_arrow_with_hook, leftwardsarrowwithhook, leftwards arrow with hook" + case .arrowRightHook: return "arrow_right_hook, rightwards arrow with hook, arrowrighthook" + case .arrowHeadingUp: return "arrow_heading_up, arrow pointing rightwards then curving upwards, arrowheadingup" + case .arrowHeadingDown: return "arrow_heading_down, arrow pointing rightwards then curving downwards, arrowheadingdown" + case .arrowsClockwise: return "clockwise downwards and upwards open circle arrows, arrowsclockwise, arrows_clockwise" + case .arrowsCounterclockwise: return "arrowscounterclockwise, arrows_counterclockwise, anticlockwise downwards and upwards open circle arrows" + case .back: return "back, back with leftwards arrow above" + case .end: return "end with leftwards arrow above, end" + case .on: return "on, on with exclamation mark with left right arrow above" + case .soon: return "soon with rightwards arrow above, soon" + case .top: return "top, top with upwards arrow above" + case .placeOfWorship: return "place_of_worship, placeofworship, place of worship" + case .atomSymbol: return "atomsymbol, atom_symbol, atom symbol" + case .omSymbol: return "omsymbol, om, om_symbol" + case .starOfDavid: return "star_of_david, star of david, starofdavid" + case .wheelOfDharma: return "wheel of dharma, wheelofdharma, wheel_of_dharma" + case .yinYang: return "yin yang, yinyang, yin_yang" + case .latinCross: return "latin_cross, latin cross, latincross" + case .orthodoxCross: return "orthodox cross, orthodoxcross, orthodox_cross" + case .starAndCrescent: return "starandcrescent, star_and_crescent, star and crescent" + case .peaceSymbol: return "peacesymbol, peace_symbol, peace symbol" + case .menorahWithNineBranches: return "menorah_with_nine_branches, menorahwithninebranches, menorah with nine branches" + case .sixPointedStar: return "six_pointed_star, six pointed star with middle dot, sixpointedstar" + case .aries: return "aries" + case .taurus: return "taurus" + case .gemini: return "gemini" + case .cancer: return "cancer" + case .leo: return "leo" + case .virgo: return "virgo" + case .libra: return "libra" + case .scorpius: return "scorpius" + case .sagittarius: return "sagittarius" + case .capricorn: return "capricorn" + case .aquarius: return "aquarius" + case .pisces: return "pisces" + case .ophiuchus: return "ophiuchus" + case .twistedRightwardsArrows: return "twisted rightwards arrows, twistedrightwardsarrows, twisted_rightwards_arrows" + case .`repeat`: return "repeat, clockwise rightwards and leftwards open circle arrows, `repeat`" + case .repeatOne: return "repeatone, repeat_one, clockwise rightwards and leftwards open circle arrows with circled one overlay" + case .arrowForward: return "arrowforward, arrow_forward, black right-pointing triangle" + case .fastForward: return "fast_forward, fastforward, black right-pointing double triangle" + case .blackRightPointingDoubleTriangleWithVerticalBar: return "next track button, black_right_pointing_double_triangle_with_vertical_bar, blackrightpointingdoubletrianglewithverticalbar" + case .blackRightPointingTriangleWithDoubleVerticalBar: return "black_right_pointing_triangle_with_double_vertical_bar, blackrightpointingtrianglewithdoubleverticalbar, play or pause button" + case .arrowBackward: return "arrow_backward, black left-pointing triangle, arrowbackward" + case .rewind: return "black left-pointing double triangle, rewind" + case .blackLeftPointingDoubleTriangleWithVerticalBar: return "last track button, blackleftpointingdoubletrianglewithverticalbar, black_left_pointing_double_triangle_with_vertical_bar" + case .arrowUpSmall: return "up-pointing small red triangle, arrowupsmall, arrow_up_small" + case .arrowDoubleUp: return "arrow_double_up, black up-pointing double triangle, arrowdoubleup" + case .arrowDownSmall: return "arrow_down_small, down-pointing small red triangle, arrowdownsmall" + case .arrowDoubleDown: return "arrowdoubledown, arrow_double_down, black down-pointing double triangle" + case .doubleVerticalBar: return "doubleverticalbar, double_vertical_bar, pause button" + case .blackSquareForStop: return "blacksquareforstop, black_square_for_stop, stop button" + case .blackCircleForRecord: return "record button, black_circle_for_record, blackcircleforrecord" + case .eject: return "eject button, eject" + case .cinema: return "cinema" + case .lowBrightness: return "lowbrightness, low brightness symbol, low_brightness" + case .highBrightness: return "high brightness symbol, highbrightness, high_brightness" + case .signalStrength: return "signal_strength, signalstrength, antenna with bars" + case .vibrationMode: return "vibration_mode, vibration mode, vibrationmode" + case .mobilePhoneOff: return "mobilephoneoff, mobile phone off, mobile_phone_off" + case .femaleSign: return "femalesign, female sign, female_sign" + case .maleSign: return "male sign, male_sign, malesign" + case .transgenderSymbol: return "transgender symbol, transgender_symbol, transgendersymbol" + case .heavyMultiplicationX: return "heavymultiplicationx, heavy_multiplication_x, heavy multiplication x" + case .heavyPlusSign: return "heavy plus sign, heavy_plus_sign, heavyplussign" + case .heavyMinusSign: return "heavy_minus_sign, heavy minus sign, heavyminussign" + case .heavyDivisionSign: return "heavy division sign, heavydivisionsign, heavy_division_sign" + case .heavyEqualsSign: return "heavy equals sign, heavyequalssign, heavy_equals_sign" + case .infinity: return "infinity" + case .bangbang: return "bangbang, double exclamation mark" + case .interrobang: return "exclamation question mark, interrobang" + case .question: return "question, black question mark ornament" + case .greyQuestion: return "greyquestion, grey_question, white question mark ornament" + case .greyExclamation: return "white exclamation mark ornament, greyexclamation, grey_exclamation" + case .exclamation: return "heavy exclamation mark symbol, exclamation, heavy_exclamation_mark" + case .wavyDash: return "wavy_dash, wavy dash, wavydash" + case .currencyExchange: return "currency exchange, currencyexchange, currency_exchange" + case .heavyDollarSign: return "heavydollarsign, heavy_dollar_sign, heavy dollar sign" + case .medicalSymbol: return "medical symbol, medical_symbol, medicalsymbol, staff_of_aesculapius" + case .recycle: return "recycle, black universal recycling symbol" + case .fleurDeLis: return "fleurdelis, fleur-de-lis, fleur_de_lis" + case .trident: return "trident, trident emblem" + case .nameBadge: return "namebadge, name_badge, name badge" + case .beginner: return "japanese symbol for beginner, beginner" + case .o: return "o, heavy large circle" + case .whiteCheckMark: return "white heavy check mark, white_check_mark, whitecheckmark" + case .ballotBoxWithCheck: return "ballotboxwithcheck, ballot_box_with_check, ballot box with check" + case .heavyCheckMark: return "heavy check mark, heavycheckmark, heavy_check_mark" + case .x: return "x, cross mark" + case .negativeSquaredCrossMark: return "negative_squared_cross_mark, negative squared cross mark, negativesquaredcrossmark" + case .curlyLoop: return "curly_loop, curlyloop, curly loop" + case .loop: return "double curly loop, loop" + case .partAlternationMark: return "part alternation mark, part_alternation_mark, partalternationmark" + case .eightSpokedAsterisk: return "eight_spoked_asterisk, eight spoked asterisk, eightspokedasterisk" + case .eightPointedBlackStar: return "eight pointed black star, eight_pointed_black_star, eightpointedblackstar" + case .sparkle: return "sparkle" + case .copyright: return "copyright, copyright sign" + case .registered: return "registered, registered sign" + case .tm: return "trade mark sign, tm" + case .hash: return "hash key, hash" + case .keycapStar: return "keycapstar, keycap_star, keycap: *" + case .zero: return "keycap 0, zero" + case .one: return "keycap 1, one" + case .two: return "two, keycap 2" + case .three: return "three, keycap 3" + case .four: return "keycap 4, four" + case .five: return "five, keycap 5" + case .six: return "six, keycap 6" + case .seven: return "seven, keycap 7" + case .eight: return "eight, keycap 8" + case .nine: return "keycap 9, nine" + case .keycapTen: return "keycap_ten, keycap ten, keycapten" + case .capitalAbcd: return "input symbol for latin capital letters, capitalabcd, capital_abcd" + case .abcd: return "abcd, input symbol for latin small letters" + case .oneTwoThreeFour: return "1234, input symbol for numbers, onetwothreefour" + case .symbols: return "input symbol for symbols, symbols" + case .abc: return "abc, input symbol for latin letters" + case .a: return "a, negative squared latin capital letter a" + case .ab: return "negative squared ab, ab" + case .b: return "b, negative squared latin capital letter b" + case .cl: return "cl, squared cl" + case .cool: return "squared cool, cool" + case .free: return "free, squared free" + case .informationSource: return "informationsource, information_source, information source" + case .id: return "id, squared id" + case .m: return "circled latin capital letter m, m" + case .new: return "squared new, new" + case .ng: return "ng, squared ng" + case .o2: return "o2, negative squared latin capital letter o" + case .ok: return "ok, squared ok" + case .parking: return "parking, negative squared latin capital letter p" + case .sos: return "sos, squared sos" + case .up: return "squared up with exclamation mark, up" + case .vs: return "squared vs, vs" + case .koko: return "squared katakana koko, koko" + case .sa: return "squared katakana sa, sa" + case .u6708: return "squared cjk unified ideograph-6708, u6708" + case .u6709: return "squared cjk unified ideograph-6709, u6709" + case .u6307: return "squared cjk unified ideograph-6307, u6307" + case .ideographAdvantage: return "circled ideograph advantage, ideograph_advantage, ideographadvantage" + case .u5272: return "u5272, squared cjk unified ideograph-5272" + case .u7121: return "u7121, squared cjk unified ideograph-7121" + case .u7981: return "u7981, squared cjk unified ideograph-7981" + case .accept: return "circled ideograph accept, accept" + case .u7533: return "squared cjk unified ideograph-7533, u7533" + case .u5408: return "squared cjk unified ideograph-5408, u5408" + case .u7a7a: return "squared cjk unified ideograph-7a7a, u7a7a" + case .congratulations: return "congratulations, circled ideograph congratulation" + case .secret: return "secret, circled ideograph secret" + case .u55b6: return "squared cjk unified ideograph-55b6, u55b6" + case .u6e80: return "squared cjk unified ideograph-6e80, u6e80" + case .redCircle: return "red_circle, large red circle, redcircle" + case .largeOrangeCircle: return "large_orange_circle, large orange circle, largeorangecircle" + case .largeYellowCircle: return "largeyellowcircle, large_yellow_circle, large yellow circle" + case .largeGreenCircle: return "largegreencircle, large green circle, large_green_circle" + case .largeBlueCircle: return "large_blue_circle, large blue circle, largebluecircle" + case .largePurpleCircle: return "large_purple_circle, large purple circle, largepurplecircle" + case .largeBrownCircle: return "largebrowncircle, large brown circle, large_brown_circle" + case .blackCircle: return "black_circle, blackcircle, medium black circle" + case .whiteCircle: return "white_circle, medium white circle, whitecircle" + case .largeRedSquare: return "large red square, largeredsquare, large_red_square" + case .largeOrangeSquare: return "large orange square, largeorangesquare, large_orange_square" + case .largeYellowSquare: return "large yellow square, large_yellow_square, largeyellowsquare" + case .largeGreenSquare: return "large_green_square, large green square, largegreensquare" + case .largeBlueSquare: return "large blue square, largebluesquare, large_blue_square" + case .largePurpleSquare: return "large purple square, large_purple_square, largepurplesquare" + case .largeBrownSquare: return "largebrownsquare, large_brown_square, large brown square" + case .blackLargeSquare: return "black_large_square, blacklargesquare, black large square" + case .whiteLargeSquare: return "whitelargesquare, white_large_square, white large square" + case .blackMediumSquare: return "black medium square, blackmediumsquare, black_medium_square" + case .whiteMediumSquare: return "white_medium_square, white medium square, whitemediumsquare" + case .blackMediumSmallSquare: return "black medium small square, blackmediumsmallsquare, black_medium_small_square" + case .whiteMediumSmallSquare: return "white_medium_small_square, whitemediumsmallsquare, white medium small square" + case .blackSmallSquare: return "black_small_square, blacksmallsquare, black small square" + case .whiteSmallSquare: return "white_small_square, white small square, whitesmallsquare" + case .largeOrangeDiamond: return "largeorangediamond, large orange diamond, large_orange_diamond" + case .largeBlueDiamond: return "large_blue_diamond, large blue diamond, largebluediamond" + case .smallOrangeDiamond: return "smallorangediamond, small orange diamond, small_orange_diamond" + case .smallBlueDiamond: return "small blue diamond, smallbluediamond, small_blue_diamond" + case .smallRedTriangle: return "small_red_triangle, up-pointing red triangle, smallredtriangle" + case .smallRedTriangleDown: return "small_red_triangle_down, down-pointing red triangle, smallredtriangledown" + case .diamondShapeWithADotInside: return "diamond shape with a dot inside, diamond_shape_with_a_dot_inside, diamondshapewithadotinside" + case .radioButton: return "radio button, radiobutton, radio_button" + case .whiteSquareButton: return "whitesquarebutton, white_square_button, white square button" + case .blackSquareButton: return "blacksquarebutton, black_square_button, black square button" + case .checkeredFlag: return "chequered flag, checkered_flag, checkeredflag" + case .triangularFlagOnPost: return "triangularflagonpost, triangular_flag_on_post, triangular flag on post" + case .crossedFlags: return "crossed flags, crossedflags, crossed_flags" + case .wavingBlackFlag: return "waving black flag, waving_black_flag, wavingblackflag" + case .wavingWhiteFlag: return "wavingwhiteflag, waving_white_flag, white flag" + case .rainbowFlag: return "rainbowflag, rainbow-flag, rainbow flag" + case .transgenderFlag: return "transgender flag, transgender_flag, transgenderflag" + case .pirateFlag: return "pirate flag, pirateflag, pirate_flag" + case .flagAc: return "flag-ac, ascension island flag, flagac" + case .flagAd: return "flagad, flag-ad, andorra flag" + case .flagAe: return "united arab emirates flag, flagae, flag-ae" + case .flagAf: return "afghanistan flag, flagaf, flag-af" + case .flagAg: return "antigua & barbuda flag, flagag, flag-ag" + case .flagAi: return "flagai, flag-ai, anguilla flag" + case .flagAl: return "flagal, flag-al, albania flag" + case .flagAm: return "armenia flag, flagam, flag-am" + case .flagAo: return "flag-ao, angola flag, flagao" + case .flagAq: return "flag-aq, antarctica flag, flagaq" + case .flagAr: return "argentina flag, flag-ar, flagar" + case .flagAs: return "american samoa flag, flag-as, flagas" + case .flagAt: return "flag-at, austria flag, flagat" + case .flagAu: return "flag-au, flagau, australia flag" + case .flagAw: return "aruba flag, flag-aw, flagaw" + case .flagAx: return "flag-ax, flagax, ÃĨland islands flag" + case .flagAz: return "flagaz, flag-az, azerbaijan flag" + case .flagBa: return "flagba, flag-ba, bosnia & herzegovina flag" + case .flagBb: return "barbados flag, flagbb, flag-bb" + case .flagBd: return "flag-bd, bangladesh flag, flagbd" + case .flagBe: return "belgium flag, flagbe, flag-be" + case .flagBf: return "burkina faso flag, flagbf, flag-bf" + case .flagBg: return "flagbg, bulgaria flag, flag-bg" + case .flagBh: return "bahrain flag, flagbh, flag-bh" + case .flagBi: return "flagbi, flag-bi, burundi flag" + case .flagBj: return "flagbj, flag-bj, benin flag" + case .flagBl: return "flag-bl, st. barthÊlemy flag, flagbl" + case .flagBm: return "bermuda flag, flag-bm, flagbm" + case .flagBn: return "flag-bn, brunei flag, flagbn" + case .flagBo: return "flag-bo, flagbo, bolivia flag" + case .flagBq: return "flagbq, caribbean netherlands flag, flag-bq" + case .flagBr: return "flag-br, flagbr, brazil flag" + case .flagBs: return "flagbs, flag-bs, bahamas flag" + case .flagBt: return "flagbt, flag-bt, bhutan flag" + case .flagBv: return "bouvet island flag, flag-bv, flagbv" + case .flagBw: return "botswana flag, flag-bw, flagbw" + case .flagBy: return "flag-by, belarus flag, flagby" + case .flagBz: return "belize flag, flag-bz, flagbz" + case .flagCa: return "flag-ca, flagca, canada flag" + case .flagCc: return "flag-cc, cocos (keeling) islands flag, flagcc" + case .flagCd: return "flag-cd, flagcd, congo - kinshasa flag" + case .flagCf: return "central african republic flag, flagcf, flag-cf" + case .flagCg: return "congo - brazzaville flag, flagcg, flag-cg" + case .flagCh: return "switzerland flag, flagch, flag-ch" + case .flagCi: return "flagci, côte d’ivoire flag, flag-ci" + case .flagCk: return "flagck, cook islands flag, flag-ck" + case .flagCl: return "flagcl, chile flag, flag-cl" + case .flagCm: return "flagcm, flag-cm, cameroon flag" + case .cn: return "china flag, cn, flag-cn" + case .flagCo: return "flagco, flag-co, colombia flag" + case .flagCp: return "clipperton island flag, flagcp, flag-cp" + case .flagCr: return "flagcr, costa rica flag, flag-cr" + case .flagCu: return "flag-cu, cuba flag, flagcu" + case .flagCv: return "flag-cv, flagcv, cape verde flag" + case .flagCw: return "flagcw, flag-cw, curaçao flag" + case .flagCx: return "christmas island flag, flag-cx, flagcx" + case .flagCy: return "cyprus flag, flagcy, flag-cy" + case .flagCz: return "flag-cz, czechia flag, flagcz" + case .de: return "flag-de, de, germany flag" + case .flagDg: return "diego garcia flag, flagdg, flag-dg" + case .flagDj: return "flag-dj, djibouti flag, flagdj" + case .flagDk: return "flag-dk, denmark flag, flagdk" + case .flagDm: return "flag-dm, flagdm, dominica flag" + case .flagDo: return "dominican republic flag, flagdo, flag-do" + case .flagDz: return "flag-dz, algeria flag, flagdz" + case .flagEa: return "ceuta & melilla flag, flagea, flag-ea" + case .flagEc: return "flag-ec, ecuador flag, flagec" + case .flagEe: return "flag-ee, estonia flag, flagee" + case .flagEg: return "egypt flag, flag-eg, flageg" + case .flagEh: return "flag-eh, western sahara flag, flageh" + case .flagEr: return "flag-er, eritrea flag, flager" + case .es: return "es, spain flag, flag-es" + case .flagEt: return "flag-et, ethiopia flag, flaget" + case .flagEu: return "flag-eu, european union flag, flageu" + case .flagFi: return "finland flag, flagfi, flag-fi" + case .flagFj: return "flagfj, flag-fj, fiji flag" + case .flagFk: return "flag-fk, flagfk, falkland islands flag" + case .flagFm: return "flagfm, flag-fm, micronesia flag" + case .flagFo: return "flag-fo, faroe islands flag, flagfo" + case .fr: return "flag-fr, france flag, fr" + case .flagGa: return "gabon flag, flag-ga, flagga" + case .gb: return "gb, uk, united kingdom flag, flag-gb" + case .flagGd: return "flaggd, flag-gd, grenada flag" + case .flagGe: return "georgia flag, flagge, flag-ge" + case .flagGf: return "flag-gf, french guiana flag, flaggf" + case .flagGg: return "guernsey flag, flaggg, flag-gg" + case .flagGh: return "flaggh, flag-gh, ghana flag" + case .flagGi: return "flag-gi, gibraltar flag, flaggi" + case .flagGl: return "flag-gl, flaggl, greenland flag" + case .flagGm: return "flag-gm, gambia flag, flaggm" + case .flagGn: return "flaggn, guinea flag, flag-gn" + case .flagGp: return "guadeloupe flag, flag-gp, flaggp" + case .flagGq: return "flag-gq, equatorial guinea flag, flaggq" + case .flagGr: return "flag-gr, flaggr, greece flag" + case .flagGs: return "flag-gs, south georgia & south sandwich islands flag, flaggs" + case .flagGt: return "flag-gt, flaggt, guatemala flag" + case .flagGu: return "flaggu, guam flag, flag-gu" + case .flagGw: return "guinea-bissau flag, flag-gw, flaggw" + case .flagGy: return "flaggy, flag-gy, guyana flag" + case .flagHk: return "flag-hk, hong kong sar china flag, flaghk" + case .flagHm: return "flag-hm, flaghm, heard & mcdonald islands flag" + case .flagHn: return "flag-hn, honduras flag, flaghn" + case .flagHr: return "flaghr, croatia flag, flag-hr" + case .flagHt: return "flag-ht, haiti flag, flaght" + case .flagHu: return "flaghu, flag-hu, hungary flag" + case .flagIc: return "flagic, flag-ic, canary islands flag" + case .flagId: return "flagid, flag-id, indonesia flag" + case .flagIe: return "ireland flag, flagie, flag-ie" + case .flagIl: return "flag-il, israel flag, flagil" + case .flagIm: return "isle of man flag, flag-im, flagim" + case .flagIn: return "india flag, flagin, flag-in" + case .flagIo: return "flag-io, british indian ocean territory flag, flagio" + case .flagIq: return "flagiq, iraq flag, flag-iq" + case .flagIr: return "flag-ir, iran flag, flagir" + case .flagIs: return "flag-is, iceland flag, flagis" + case .it: return "it, italy flag, flag-it" + case .flagJe: return "jersey flag, flagje, flag-je" + case .flagJm: return "flag-jm, jamaica flag, flagjm" + case .flagJo: return "flag-jo, jordan flag, flagjo" + case .jp: return "japan flag, jp, flag-jp" + case .flagKe: return "kenya flag, flag-ke, flagke" + case .flagKg: return "kyrgyzstan flag, flagkg, flag-kg" + case .flagKh: return "cambodia flag, flagkh, flag-kh" + case .flagKi: return "flagki, flag-ki, kiribati flag" + case .flagKm: return "flag-km, comoros flag, flagkm" + case .flagKn: return "st. kitts & nevis flag, flagkn, flag-kn" + case .flagKp: return "flag-kp, north korea flag, flagkp" + case .kr: return "kr, south korea flag, flag-kr" + case .flagKw: return "flag-kw, flagkw, kuwait flag" + case .flagKy: return "flagky, cayman islands flag, flag-ky" + case .flagKz: return "flag-kz, kazakhstan flag, flagkz" + case .flagLa: return "flagla, flag-la, laos flag" + case .flagLb: return "flaglb, lebanon flag, flag-lb" + case .flagLc: return "st. lucia flag, flag-lc, flaglc" + case .flagLi: return "flag-li, liechtenstein flag, flagli" + case .flagLk: return "flag-lk, flaglk, sri lanka flag" + case .flagLr: return "flaglr, liberia flag, flag-lr" + case .flagLs: return "flagls, flag-ls, lesotho flag" + case .flagLt: return "lithuania flag, flaglt, flag-lt" + case .flagLu: return "luxembourg flag, flaglu, flag-lu" + case .flagLv: return "flaglv, latvia flag, flag-lv" + case .flagLy: return "libya flag, flagly, flag-ly" + case .flagMa: return "flag-ma, morocco flag, flagma" + case .flagMc: return "flag-mc, monaco flag, flagmc" + case .flagMd: return "flagmd, flag-md, moldova flag" + case .flagMe: return "montenegro flag, flag-me, flagme" + case .flagMf: return "flagmf, flag-mf, st. martin flag" + case .flagMg: return "flag-mg, madagascar flag, flagmg" + case .flagMh: return "flag-mh, flagmh, marshall islands flag" + case .flagMk: return "flagmk, flag-mk, north macedonia flag" + case .flagMl: return "flag-ml, mali flag, flagml" + case .flagMm: return "flag-mm, flagmm, myanmar (burma) flag" + case .flagMn: return "flag-mn, flagmn, mongolia flag" + case .flagMo: return "flag-mo, macao sar china flag, flagmo" + case .flagMp: return "flagmp, northern mariana islands flag, flag-mp" + case .flagMq: return "martinique flag, flagmq, flag-mq" + case .flagMr: return "flagmr, mauritania flag, flag-mr" + case .flagMs: return "montserrat flag, flagms, flag-ms" + case .flagMt: return "flag-mt, malta flag, flagmt" + case .flagMu: return "flag-mu, flagmu, mauritius flag" + case .flagMv: return "maldives flag, flag-mv, flagmv" + case .flagMw: return "flagmw, flag-mw, malawi flag" + case .flagMx: return "mexico flag, flagmx, flag-mx" + case .flagMy: return "flag-my, malaysia flag, flagmy" + case .flagMz: return "flagmz, flag-mz, mozambique flag" + case .flagNa: return "flagna, namibia flag, flag-na" + case .flagNc: return "new caledonia flag, flagnc, flag-nc" + case .flagNe: return "flagne, niger flag, flag-ne" + case .flagNf: return "flagnf, flag-nf, norfolk island flag" + case .flagNg: return "nigeria flag, flag-ng, flagng" + case .flagNi: return "flag-ni, nicaragua flag, flagni" + case .flagNl: return "flag-nl, netherlands flag, flagnl" + case .flagNo: return "norway flag, flagno, flag-no" + case .flagNp: return "flagnp, flag-np, nepal flag" + case .flagNr: return "flagnr, flag-nr, nauru flag" + case .flagNu: return "flag-nu, niue flag, flagnu" + case .flagNz: return "new zealand flag, flagnz, flag-nz" + case .flagOm: return "flagom, oman flag, flag-om" + case .flagPa: return "panama flag, flagpa, flag-pa" + case .flagPe: return "peru flag, flagpe, flag-pe" + case .flagPf: return "flagpf, flag-pf, french polynesia flag" + case .flagPg: return "flagpg, flag-pg, papua new guinea flag" + case .flagPh: return "flag-ph, flagph, philippines flag" + case .flagPk: return "flagpk, flag-pk, pakistan flag" + case .flagPl: return "flag-pl, flagpl, poland flag" + case .flagPm: return "flag-pm, st. pierre & miquelon flag, flagpm" + case .flagPn: return "flagpn, pitcairn islands flag, flag-pn" + case .flagPr: return "puerto rico flag, flagpr, flag-pr" + case .flagPs: return "flag-ps, palestinian territories flag, flagps" + case .flagPt: return "flag-pt, portugal flag, flagpt" + case .flagPw: return "palau flag, flagpw, flag-pw" + case .flagPy: return "flagpy, flag-py, paraguay flag" + case .flagQa: return "flagqa, qatar flag, flag-qa" + case .flagRe: return "flag-re, flagre, rÊunion flag" + case .flagRo: return "flag-ro, romania flag, flagro" + case .flagRs: return "flagrs, flag-rs, serbia flag" + case .ru: return "russia flag, ru, flag-ru" + case .flagRw: return "rwanda flag, flag-rw, flagrw" + case .flagSa: return "flag-sa, flagsa, saudi arabia flag" + case .flagSb: return "solomon islands flag, flag-sb, flagsb" + case .flagSc: return "flagsc, seychelles flag, flag-sc" + case .flagSd: return "flag-sd, flagsd, sudan flag" + case .flagSe: return "flag-se, sweden flag, flagse" + case .flagSg: return "flag-sg, singapore flag, flagsg" + case .flagSh: return "flagsh, st. helena flag, flag-sh" + case .flagSi: return "flag-si, slovenia flag, flagsi" + case .flagSj: return "flag-sj, svalbard & jan mayen flag, flagsj" + case .flagSk: return "slovakia flag, flagsk, flag-sk" + case .flagSl: return "sierra leone flag, flag-sl, flagsl" + case .flagSm: return "flag-sm, san marino flag, flagsm" + case .flagSn: return "senegal flag, flagsn, flag-sn" + case .flagSo: return "flagso, flag-so, somalia flag" + case .flagSr: return "flag-sr, suriname flag, flagsr" + case .flagSs: return "flag-ss, south sudan flag, flagss" + case .flagSt: return "flagst, flag-st, sÃŖo tomÊ & príncipe flag" + case .flagSv: return "flag-sv, el salvador flag, flagsv" + case .flagSx: return "flag-sx, sint maarten flag, flagsx" + case .flagSy: return "flag-sy, syria flag, flagsy" + case .flagSz: return "flagsz, eswatini flag, flag-sz" + case .flagTa: return "flag-ta, flagta, tristan da cunha flag" + case .flagTc: return "flagtc, turks & caicos islands flag, flag-tc" + case .flagTd: return "flagtd, flag-td, chad flag" + case .flagTf: return "flag-tf, french southern territories flag, flagtf" + case .flagTg: return "flagtg, togo flag, flag-tg" + case .flagTh: return "thailand flag, flagth, flag-th" + case .flagTj: return "tajikistan flag, flagtj, flag-tj" + case .flagTk: return "tokelau flag, flag-tk, flagtk" + case .flagTl: return "flag-tl, timor-leste flag, flagtl" + case .flagTm: return "flag-tm, turkmenistan flag, flagtm" + case .flagTn: return "flagtn, tunisia flag, flag-tn" + case .flagTo: return "flag-to, flagto, tonga flag" + case .flagTr: return "flagtr, flag-tr, turkey flag" + case .flagTt: return "flag-tt, trinidad & tobago flag, flagtt" + case .flagTv: return "tuvalu flag, flag-tv, flagtv" + case .flagTw: return "flag-tw, taiwan flag, flagtw" + case .flagTz: return "flag-tz, flagtz, tanzania flag" + case .flagUa: return "ukraine flag, flagua, flag-ua" + case .flagUg: return "flagug, uganda flag, flag-ug" + case .flagUm: return "flag-um, flagum, u.s. outlying islands flag" + case .flagUn: return "united nations flag, flag-un, flagun" + case .us: return "flag-us, us, united states flag" + case .flagUy: return "flaguy, uruguay flag, flag-uy" + case .flagUz: return "flag-uz, uzbekistan flag, flaguz" + case .flagVa: return "flag-va, flagva, vatican city flag" + case .flagVc: return "flag-vc, st. vincent & grenadines flag, flagvc" + case .flagVe: return "flag-ve, venezuela flag, flagve" + case .flagVg: return "flag-vg, flagvg, british virgin islands flag" + case .flagVi: return "flagvi, u.s. virgin islands flag, flag-vi" + case .flagVn: return "flagvn, flag-vn, vietnam flag" + case .flagVu: return "flagvu, vanuatu flag, flag-vu" + case .flagWf: return "flag-wf, wallis & futuna flag, flagwf" + case .flagWs: return "flag-ws, samoa flag, flagws" + case .flagXk: return "flagxk, kosovo flag, flag-xk" + case .flagYe: return "flagye, yemen flag, flag-ye" + case .flagYt: return "flag-yt, flagyt, mayotte flag" + case .flagZa: return "south africa flag, flagza, flag-za" + case .flagZm: return "flag-zm, zambia flag, flagzm" + case .flagZw: return "flagzw, zimbabwe flag, flag-zw" + case .flagEngland: return "flagengland, england flag, flag-england" + case .flagScotland: return "scotland flag, flagscotland, flag-scotland" + case .flagWales: return "flagwales, flag-wales, wales flag" + } + } +} diff --git a/Session/Emoji/Emoji+SkinTones.swift b/Session/Emoji/Emoji+SkinTones.swift new file mode 100644 index 000000000..e9aaec044 --- /dev/null +++ b/Session/Emoji/Emoji+SkinTones.swift @@ -0,0 +1,2724 @@ + +// This file is generated by EmojiGenerator.swift, do not manually edit it. + +extension Emoji { + enum SkinTone: String, CaseIterable, Equatable { + case light = "đŸģ" + case mediumLight = "đŸŧ" + case medium = "đŸŊ" + case mediumDark = "🏾" + case dark = "đŸŋ" + } + + var hasSkinTones: Bool { return emojiPerSkinTonePermutation != nil } + var allowsMultipleSkinTones: Bool { return hasSkinTones && skinToneComponentEmoji != nil } + + var skinToneComponentEmoji: [Emoji]? { + switch self { + case .handshake: return [.rightwardsHand, .leftwardsHand] + case .peopleHoldingHands: return [.standingPerson, .standingPerson] + case .twoWomenHoldingHands: return [.womanStanding, .womanStanding] + case .manAndWomanHoldingHands: return [.womanStanding, .manStanding] + case .twoMenHoldingHands: return [.manStanding, .manStanding] + case .personKissPerson: return [.adult, .adult] + case .womanKissMan: return [.woman, .man] + case .manKissMan: return [.man, .man] + case .womanKissWoman: return [.woman, .woman] + case .personHeartPerson: return [.adult, .adult] + case .womanHeartMan: return [.woman, .man] + case .manHeartMan: return [.man, .man] + case .womanHeartWoman: return [.woman, .woman] + default: return nil + } + } + + var emojiPerSkinTonePermutation: [[SkinTone]: String]? { + switch self { + case .wave: + return [ + [.light]: "👋đŸģ", + [.mediumLight]: "👋đŸŧ", + [.medium]: "👋đŸŊ", + [.mediumDark]: "👋🏾", + [.dark]: "👋đŸŋ", + ] + case .raisedBackOfHand: + return [ + [.light]: "🤚đŸģ", + [.mediumLight]: "🤚đŸŧ", + [.medium]: "🤚đŸŊ", + [.mediumDark]: "🤚🏾", + [.dark]: "🤚đŸŋ", + ] + case .raisedHandWithFingersSplayed: + return [ + [.light]: "🖐đŸģ", + [.mediumLight]: "🖐đŸŧ", + [.medium]: "🖐đŸŊ", + [.mediumDark]: "🖐🏾", + [.dark]: "🖐đŸŋ", + ] + case .hand: + return [ + [.light]: "✋đŸģ", + [.mediumLight]: "✋đŸŧ", + [.medium]: "✋đŸŊ", + [.mediumDark]: "✋🏾", + [.dark]: "✋đŸŋ", + ] + case .spockHand: + return [ + [.light]: "🖖đŸģ", + [.mediumLight]: "🖖đŸŧ", + [.medium]: "🖖đŸŊ", + [.mediumDark]: "🖖🏾", + [.dark]: "🖖đŸŋ", + ] + case .rightwardsHand: + return [ + [.light]: "đŸĢąđŸģ", + [.mediumLight]: "đŸĢąđŸŧ", + [.medium]: "đŸĢąđŸŊ", + [.mediumDark]: "đŸĢąđŸž", + [.dark]: "đŸĢąđŸŋ", + ] + case .leftwardsHand: + return [ + [.light]: "đŸĢ˛đŸģ", + [.mediumLight]: "đŸĢ˛đŸŧ", + [.medium]: "đŸĢ˛đŸŊ", + [.mediumDark]: "đŸĢ˛đŸž", + [.dark]: "đŸĢ˛đŸŋ", + ] + case .palmDownHand: + return [ + [.light]: "đŸĢŗđŸģ", + [.mediumLight]: "đŸĢŗđŸŧ", + [.medium]: "đŸĢŗđŸŊ", + [.mediumDark]: "đŸĢŗ🏾", + [.dark]: "đŸĢŗđŸŋ", + ] + case .palmUpHand: + return [ + [.light]: "đŸĢ´đŸģ", + [.mediumLight]: "đŸĢ´đŸŧ", + [.medium]: "đŸĢ´đŸŊ", + [.mediumDark]: "đŸĢ´đŸž", + [.dark]: "đŸĢ´đŸŋ", + ] + case .okHand: + return [ + [.light]: "👌đŸģ", + [.mediumLight]: "👌đŸŧ", + [.medium]: "👌đŸŊ", + [.mediumDark]: "👌🏾", + [.dark]: "👌đŸŋ", + ] + case .pinchedFingers: + return [ + [.light]: "🤌đŸģ", + [.mediumLight]: "🤌đŸŧ", + [.medium]: "🤌đŸŊ", + [.mediumDark]: "🤌🏾", + [.dark]: "🤌đŸŋ", + ] + case .pinchingHand: + return [ + [.light]: "🤏đŸģ", + [.mediumLight]: "🤏đŸŧ", + [.medium]: "🤏đŸŊ", + [.mediumDark]: "🤏🏾", + [.dark]: "🤏đŸŋ", + ] + case .v: + return [ + [.light]: "✌đŸģ", + [.mediumLight]: "✌đŸŧ", + [.medium]: "✌đŸŊ", + [.mediumDark]: "✌🏾", + [.dark]: "✌đŸŋ", + ] + case .crossedFingers: + return [ + [.light]: "🤞đŸģ", + [.mediumLight]: "🤞đŸŧ", + [.medium]: "🤞đŸŊ", + [.mediumDark]: "🤞🏾", + [.dark]: "🤞đŸŋ", + ] + case .handWithIndexFingerAndThumbCrossed: + return [ + [.light]: "đŸĢ°đŸģ", + [.mediumLight]: "đŸĢ°đŸŧ", + [.medium]: "đŸĢ°đŸŊ", + [.mediumDark]: "đŸĢ°đŸž", + [.dark]: "đŸĢ°đŸŋ", + ] + case .iLoveYouHandSign: + return [ + [.light]: "🤟đŸģ", + [.mediumLight]: "🤟đŸŧ", + [.medium]: "🤟đŸŊ", + [.mediumDark]: "🤟🏾", + [.dark]: "🤟đŸŋ", + ] + case .theHorns: + return [ + [.light]: "🤘đŸģ", + [.mediumLight]: "🤘đŸŧ", + [.medium]: "🤘đŸŊ", + [.mediumDark]: "🤘🏾", + [.dark]: "🤘đŸŋ", + ] + case .callMeHand: + return [ + [.light]: "🤙đŸģ", + [.mediumLight]: "🤙đŸŧ", + [.medium]: "🤙đŸŊ", + [.mediumDark]: "🤙🏾", + [.dark]: "🤙đŸŋ", + ] + case .pointLeft: + return [ + [.light]: "👈đŸģ", + [.mediumLight]: "👈đŸŧ", + [.medium]: "👈đŸŊ", + [.mediumDark]: "👈🏾", + [.dark]: "👈đŸŋ", + ] + case .pointRight: + return [ + [.light]: "👉đŸģ", + [.mediumLight]: "👉đŸŧ", + [.medium]: "👉đŸŊ", + [.mediumDark]: "👉🏾", + [.dark]: "👉đŸŋ", + ] + case .pointUp2: + return [ + [.light]: "👆đŸģ", + [.mediumLight]: "👆đŸŧ", + [.medium]: "👆đŸŊ", + [.mediumDark]: "👆🏾", + [.dark]: "👆đŸŋ", + ] + case .middleFinger: + return [ + [.light]: "🖕đŸģ", + [.mediumLight]: "🖕đŸŧ", + [.medium]: "🖕đŸŊ", + [.mediumDark]: "🖕🏾", + [.dark]: "🖕đŸŋ", + ] + case .pointDown: + return [ + [.light]: "👇đŸģ", + [.mediumLight]: "👇đŸŧ", + [.medium]: "👇đŸŊ", + [.mediumDark]: "👇🏾", + [.dark]: "👇đŸŋ", + ] + case .pointUp: + return [ + [.light]: "☝đŸģ", + [.mediumLight]: "☝đŸŧ", + [.medium]: "☝đŸŊ", + [.mediumDark]: "☝🏾", + [.dark]: "☝đŸŋ", + ] + case .indexPointingAtTheViewer: + return [ + [.light]: "đŸĢĩđŸģ", + [.mediumLight]: "đŸĢĩđŸŧ", + [.medium]: "đŸĢĩđŸŊ", + [.mediumDark]: "đŸĢĩ🏾", + [.dark]: "đŸĢĩđŸŋ", + ] + case .plusOne: + return [ + [.light]: "👍đŸģ", + [.mediumLight]: "👍đŸŧ", + [.medium]: "👍đŸŊ", + [.mediumDark]: "👍🏾", + [.dark]: "👍đŸŋ", + ] + case .negativeOne: + return [ + [.light]: "👎đŸģ", + [.mediumLight]: "👎đŸŧ", + [.medium]: "👎đŸŊ", + [.mediumDark]: "👎🏾", + [.dark]: "👎đŸŋ", + ] + case .fist: + return [ + [.light]: "✊đŸģ", + [.mediumLight]: "✊đŸŧ", + [.medium]: "✊đŸŊ", + [.mediumDark]: "✊🏾", + [.dark]: "✊đŸŋ", + ] + case .facepunch: + return [ + [.light]: "👊đŸģ", + [.mediumLight]: "👊đŸŧ", + [.medium]: "👊đŸŊ", + [.mediumDark]: "👊🏾", + [.dark]: "👊đŸŋ", + ] + case .leftFacingFist: + return [ + [.light]: "🤛đŸģ", + [.mediumLight]: "🤛đŸŧ", + [.medium]: "🤛đŸŊ", + [.mediumDark]: "🤛🏾", + [.dark]: "🤛đŸŋ", + ] + case .rightFacingFist: + return [ + [.light]: "🤜đŸģ", + [.mediumLight]: "🤜đŸŧ", + [.medium]: "🤜đŸŊ", + [.mediumDark]: "🤜🏾", + [.dark]: "🤜đŸŋ", + ] + case .clap: + return [ + [.light]: "👏đŸģ", + [.mediumLight]: "👏đŸŧ", + [.medium]: "👏đŸŊ", + [.mediumDark]: "👏🏾", + [.dark]: "👏đŸŋ", + ] + case .raisedHands: + return [ + [.light]: "🙌đŸģ", + [.mediumLight]: "🙌đŸŧ", + [.medium]: "🙌đŸŊ", + [.mediumDark]: "🙌🏾", + [.dark]: "🙌đŸŋ", + ] + case .heartHands: + return [ + [.light]: "đŸĢļđŸģ", + [.mediumLight]: "đŸĢļđŸŧ", + [.medium]: "đŸĢļđŸŊ", + [.mediumDark]: "đŸĢļ🏾", + [.dark]: "đŸĢļđŸŋ", + ] + case .openHands: + return [ + [.light]: "👐đŸģ", + [.mediumLight]: "👐đŸŧ", + [.medium]: "👐đŸŊ", + [.mediumDark]: "👐🏾", + [.dark]: "👐đŸŋ", + ] + case .palmsUpTogether: + return [ + [.light]: "🤲đŸģ", + [.mediumLight]: "🤲đŸŧ", + [.medium]: "🤲đŸŊ", + [.mediumDark]: "🤲🏾", + [.dark]: "🤲đŸŋ", + ] + case .handshake: + return [ + [.light]: "🤝đŸģ", + [.light, .mediumLight]: "đŸĢąđŸģ‍đŸĢ˛đŸŧ", + [.light, .medium]: "đŸĢąđŸģ‍đŸĢ˛đŸŊ", + [.light, .mediumDark]: "đŸĢąđŸģ‍đŸĢ˛đŸž", + [.light, .dark]: "đŸĢąđŸģ‍đŸĢ˛đŸŋ", + [.mediumLight]: "🤝đŸŧ", + [.mediumLight, .light]: "đŸĢąđŸŧ‍đŸĢ˛đŸģ", + [.mediumLight, .medium]: "đŸĢąđŸŧ‍đŸĢ˛đŸŊ", + [.mediumLight, .mediumDark]: "đŸĢąđŸŧ‍đŸĢ˛đŸž", + [.mediumLight, .dark]: "đŸĢąđŸŧ‍đŸĢ˛đŸŋ", + [.medium]: "🤝đŸŊ", + [.medium, .light]: "đŸĢąđŸŊ‍đŸĢ˛đŸģ", + [.medium, .mediumLight]: "đŸĢąđŸŊ‍đŸĢ˛đŸŧ", + [.medium, .mediumDark]: "đŸĢąđŸŊ‍đŸĢ˛đŸž", + [.medium, .dark]: "đŸĢąđŸŊ‍đŸĢ˛đŸŋ", + [.mediumDark]: "🤝🏾", + [.mediumDark, .light]: "đŸĢąđŸžâ€đŸĢ˛đŸģ", + [.mediumDark, .mediumLight]: "đŸĢąđŸžâ€đŸĢ˛đŸŧ", + [.mediumDark, .medium]: "đŸĢąđŸžâ€đŸĢ˛đŸŊ", + [.mediumDark, .dark]: "đŸĢąđŸžâ€đŸĢ˛đŸŋ", + [.dark]: "🤝đŸŋ", + [.dark, .light]: "đŸĢąđŸŋ‍đŸĢ˛đŸģ", + [.dark, .mediumLight]: "đŸĢąđŸŋ‍đŸĢ˛đŸŧ", + [.dark, .medium]: "đŸĢąđŸŋ‍đŸĢ˛đŸŊ", + [.dark, .mediumDark]: "đŸĢąđŸŋ‍đŸĢ˛đŸž", + ] + case .pray: + return [ + [.light]: "🙏đŸģ", + [.mediumLight]: "🙏đŸŧ", + [.medium]: "🙏đŸŊ", + [.mediumDark]: "🙏🏾", + [.dark]: "🙏đŸŋ", + ] + case .writingHand: + return [ + [.light]: "✍đŸģ", + [.mediumLight]: "✍đŸŧ", + [.medium]: "✍đŸŊ", + [.mediumDark]: "✍🏾", + [.dark]: "✍đŸŋ", + ] + case .nailCare: + return [ + [.light]: "💅đŸģ", + [.mediumLight]: "💅đŸŧ", + [.medium]: "💅đŸŊ", + [.mediumDark]: "💅🏾", + [.dark]: "💅đŸŋ", + ] + case .selfie: + return [ + [.light]: "đŸ¤ŗđŸģ", + [.mediumLight]: "đŸ¤ŗđŸŧ", + [.medium]: "đŸ¤ŗđŸŊ", + [.mediumDark]: "đŸ¤ŗ🏾", + [.dark]: "đŸ¤ŗđŸŋ", + ] + case .muscle: + return [ + [.light]: "đŸ’ĒđŸģ", + [.mediumLight]: "đŸ’ĒđŸŧ", + [.medium]: "đŸ’ĒđŸŊ", + [.mediumDark]: "đŸ’Ē🏾", + [.dark]: "đŸ’ĒđŸŋ", + ] + case .leg: + return [ + [.light]: "đŸĻĩđŸģ", + [.mediumLight]: "đŸĻĩđŸŧ", + [.medium]: "đŸĻĩđŸŊ", + [.mediumDark]: "đŸĻĩ🏾", + [.dark]: "đŸĻĩđŸŋ", + ] + case .foot: + return [ + [.light]: "đŸĻļđŸģ", + [.mediumLight]: "đŸĻļđŸŧ", + [.medium]: "đŸĻļđŸŊ", + [.mediumDark]: "đŸĻļ🏾", + [.dark]: "đŸĻļđŸŋ", + ] + case .ear: + return [ + [.light]: "👂đŸģ", + [.mediumLight]: "👂đŸŧ", + [.medium]: "👂đŸŊ", + [.mediumDark]: "👂🏾", + [.dark]: "👂đŸŋ", + ] + case .earWithHearingAid: + return [ + [.light]: "đŸĻģđŸģ", + [.mediumLight]: "đŸĻģđŸŧ", + [.medium]: "đŸĻģđŸŊ", + [.mediumDark]: "đŸĻģ🏾", + [.dark]: "đŸĻģđŸŋ", + ] + case .nose: + return [ + [.light]: "👃đŸģ", + [.mediumLight]: "👃đŸŧ", + [.medium]: "👃đŸŊ", + [.mediumDark]: "👃🏾", + [.dark]: "👃đŸŋ", + ] + case .baby: + return [ + [.light]: "đŸ‘ļđŸģ", + [.mediumLight]: "đŸ‘ļđŸŧ", + [.medium]: "đŸ‘ļđŸŊ", + [.mediumDark]: "đŸ‘ļ🏾", + [.dark]: "đŸ‘ļđŸŋ", + ] + case .child: + return [ + [.light]: "🧒đŸģ", + [.mediumLight]: "🧒đŸŧ", + [.medium]: "🧒đŸŊ", + [.mediumDark]: "🧒🏾", + [.dark]: "🧒đŸŋ", + ] + case .boy: + return [ + [.light]: "đŸ‘ĻđŸģ", + [.mediumLight]: "đŸ‘ĻđŸŧ", + [.medium]: "đŸ‘ĻđŸŊ", + [.mediumDark]: "đŸ‘Ļ🏾", + [.dark]: "đŸ‘ĻđŸŋ", + ] + case .girl: + return [ + [.light]: "👧đŸģ", + [.mediumLight]: "👧đŸŧ", + [.medium]: "👧đŸŊ", + [.mediumDark]: "👧🏾", + [.dark]: "👧đŸŋ", + ] + case .adult: + return [ + [.light]: "🧑đŸģ", + [.mediumLight]: "🧑đŸŧ", + [.medium]: "🧑đŸŊ", + [.mediumDark]: "🧑🏾", + [.dark]: "🧑đŸŋ", + ] + case .personWithBlondHair: + return [ + [.light]: "👱đŸģ", + [.mediumLight]: "👱đŸŧ", + [.medium]: "👱đŸŊ", + [.mediumDark]: "👱🏾", + [.dark]: "👱đŸŋ", + ] + case .man: + return [ + [.light]: "👨đŸģ", + [.mediumLight]: "👨đŸŧ", + [.medium]: "👨đŸŊ", + [.mediumDark]: "👨🏾", + [.dark]: "👨đŸŋ", + ] + case .beardedPerson: + return [ + [.light]: "🧔đŸģ", + [.mediumLight]: "🧔đŸŧ", + [.medium]: "🧔đŸŊ", + [.mediumDark]: "🧔🏾", + [.dark]: "🧔đŸŋ", + ] + case .manWithBeard: + return [ + [.light]: "🧔đŸģ‍♂ī¸", + [.mediumLight]: "🧔đŸŧ‍♂ī¸", + [.medium]: "🧔đŸŊ‍♂ī¸", + [.mediumDark]: "🧔🏾‍♂ī¸", + [.dark]: "🧔đŸŋ‍♂ī¸", + ] + case .womanWithBeard: + return [ + [.light]: "🧔đŸģ‍♀ī¸", + [.mediumLight]: "🧔đŸŧ‍♀ī¸", + [.medium]: "🧔đŸŊ‍♀ī¸", + [.mediumDark]: "🧔🏾‍♀ī¸", + [.dark]: "🧔đŸŋ‍♀ī¸", + ] + case .redHairedMan: + return [ + [.light]: "👨đŸģ‍đŸĻ°", + [.mediumLight]: "👨đŸŧ‍đŸĻ°", + [.medium]: "👨đŸŊ‍đŸĻ°", + [.mediumDark]: "👨🏾‍đŸĻ°", + [.dark]: "👨đŸŋ‍đŸĻ°", + ] + case .curlyHairedMan: + return [ + [.light]: "👨đŸģ‍đŸĻą", + [.mediumLight]: "👨đŸŧ‍đŸĻą", + [.medium]: "👨đŸŊ‍đŸĻą", + [.mediumDark]: "👨🏾‍đŸĻą", + [.dark]: "👨đŸŋ‍đŸĻą", + ] + case .whiteHairedMan: + return [ + [.light]: "👨đŸģ‍đŸĻŗ", + [.mediumLight]: "👨đŸŧ‍đŸĻŗ", + [.medium]: "👨đŸŊ‍đŸĻŗ", + [.mediumDark]: "👨🏾‍đŸĻŗ", + [.dark]: "👨đŸŋ‍đŸĻŗ", + ] + case .baldMan: + return [ + [.light]: "👨đŸģ‍đŸĻ˛", + [.mediumLight]: "👨đŸŧ‍đŸĻ˛", + [.medium]: "👨đŸŊ‍đŸĻ˛", + [.mediumDark]: "👨🏾‍đŸĻ˛", + [.dark]: "👨đŸŋ‍đŸĻ˛", + ] + case .woman: + return [ + [.light]: "👩đŸģ", + [.mediumLight]: "👩đŸŧ", + [.medium]: "👩đŸŊ", + [.mediumDark]: "👩🏾", + [.dark]: "👩đŸŋ", + ] + case .redHairedWoman: + return [ + [.light]: "👩đŸģ‍đŸĻ°", + [.mediumLight]: "👩đŸŧ‍đŸĻ°", + [.medium]: "👩đŸŊ‍đŸĻ°", + [.mediumDark]: "👩🏾‍đŸĻ°", + [.dark]: "👩đŸŋ‍đŸĻ°", + ] + case .redHairedPerson: + return [ + [.light]: "🧑đŸģ‍đŸĻ°", + [.mediumLight]: "🧑đŸŧ‍đŸĻ°", + [.medium]: "🧑đŸŊ‍đŸĻ°", + [.mediumDark]: "🧑🏾‍đŸĻ°", + [.dark]: "🧑đŸŋ‍đŸĻ°", + ] + case .curlyHairedWoman: + return [ + [.light]: "👩đŸģ‍đŸĻą", + [.mediumLight]: "👩đŸŧ‍đŸĻą", + [.medium]: "👩đŸŊ‍đŸĻą", + [.mediumDark]: "👩🏾‍đŸĻą", + [.dark]: "👩đŸŋ‍đŸĻą", + ] + case .curlyHairedPerson: + return [ + [.light]: "🧑đŸģ‍đŸĻą", + [.mediumLight]: "🧑đŸŧ‍đŸĻą", + [.medium]: "🧑đŸŊ‍đŸĻą", + [.mediumDark]: "🧑🏾‍đŸĻą", + [.dark]: "🧑đŸŋ‍đŸĻą", + ] + case .whiteHairedWoman: + return [ + [.light]: "👩đŸģ‍đŸĻŗ", + [.mediumLight]: "👩đŸŧ‍đŸĻŗ", + [.medium]: "👩đŸŊ‍đŸĻŗ", + [.mediumDark]: "👩🏾‍đŸĻŗ", + [.dark]: "👩đŸŋ‍đŸĻŗ", + ] + case .whiteHairedPerson: + return [ + [.light]: "🧑đŸģ‍đŸĻŗ", + [.mediumLight]: "🧑đŸŧ‍đŸĻŗ", + [.medium]: "🧑đŸŊ‍đŸĻŗ", + [.mediumDark]: "🧑🏾‍đŸĻŗ", + [.dark]: "🧑đŸŋ‍đŸĻŗ", + ] + case .baldWoman: + return [ + [.light]: "👩đŸģ‍đŸĻ˛", + [.mediumLight]: "👩đŸŧ‍đŸĻ˛", + [.medium]: "👩đŸŊ‍đŸĻ˛", + [.mediumDark]: "👩🏾‍đŸĻ˛", + [.dark]: "👩đŸŋ‍đŸĻ˛", + ] + case .baldPerson: + return [ + [.light]: "🧑đŸģ‍đŸĻ˛", + [.mediumLight]: "🧑đŸŧ‍đŸĻ˛", + [.medium]: "🧑đŸŊ‍đŸĻ˛", + [.mediumDark]: "🧑🏾‍đŸĻ˛", + [.dark]: "🧑đŸŋ‍đŸĻ˛", + ] + case .blondHairedWoman: + return [ + [.light]: "👱đŸģ‍♀ī¸", + [.mediumLight]: "👱đŸŧ‍♀ī¸", + [.medium]: "👱đŸŊ‍♀ī¸", + [.mediumDark]: "👱🏾‍♀ī¸", + [.dark]: "👱đŸŋ‍♀ī¸", + ] + case .blondHairedMan: + return [ + [.light]: "👱đŸģ‍♂ī¸", + [.mediumLight]: "👱đŸŧ‍♂ī¸", + [.medium]: "👱đŸŊ‍♂ī¸", + [.mediumDark]: "👱🏾‍♂ī¸", + [.dark]: "👱đŸŋ‍♂ī¸", + ] + case .olderAdult: + return [ + [.light]: "🧓đŸģ", + [.mediumLight]: "🧓đŸŧ", + [.medium]: "🧓đŸŊ", + [.mediumDark]: "🧓🏾", + [.dark]: "🧓đŸŋ", + ] + case .olderMan: + return [ + [.light]: "👴đŸģ", + [.mediumLight]: "👴đŸŧ", + [.medium]: "👴đŸŊ", + [.mediumDark]: "👴🏾", + [.dark]: "👴đŸŋ", + ] + case .olderWoman: + return [ + [.light]: "đŸ‘ĩđŸģ", + [.mediumLight]: "đŸ‘ĩđŸŧ", + [.medium]: "đŸ‘ĩđŸŊ", + [.mediumDark]: "đŸ‘ĩ🏾", + [.dark]: "đŸ‘ĩđŸŋ", + ] + case .personFrowning: + return [ + [.light]: "🙍đŸģ", + [.mediumLight]: "🙍đŸŧ", + [.medium]: "🙍đŸŊ", + [.mediumDark]: "🙍🏾", + [.dark]: "🙍đŸŋ", + ] + case .manFrowning: + return [ + [.light]: "🙍đŸģ‍♂ī¸", + [.mediumLight]: "🙍đŸŧ‍♂ī¸", + [.medium]: "🙍đŸŊ‍♂ī¸", + [.mediumDark]: "🙍🏾‍♂ī¸", + [.dark]: "🙍đŸŋ‍♂ī¸", + ] + case .womanFrowning: + return [ + [.light]: "🙍đŸģ‍♀ī¸", + [.mediumLight]: "🙍đŸŧ‍♀ī¸", + [.medium]: "🙍đŸŊ‍♀ī¸", + [.mediumDark]: "🙍🏾‍♀ī¸", + [.dark]: "🙍đŸŋ‍♀ī¸", + ] + case .personWithPoutingFace: + return [ + [.light]: "🙎đŸģ", + [.mediumLight]: "🙎đŸŧ", + [.medium]: "🙎đŸŊ", + [.mediumDark]: "🙎🏾", + [.dark]: "🙎đŸŋ", + ] + case .manPouting: + return [ + [.light]: "🙎đŸģ‍♂ī¸", + [.mediumLight]: "🙎đŸŧ‍♂ī¸", + [.medium]: "🙎đŸŊ‍♂ī¸", + [.mediumDark]: "🙎🏾‍♂ī¸", + [.dark]: "🙎đŸŋ‍♂ī¸", + ] + case .womanPouting: + return [ + [.light]: "🙎đŸģ‍♀ī¸", + [.mediumLight]: "🙎đŸŧ‍♀ī¸", + [.medium]: "🙎đŸŊ‍♀ī¸", + [.mediumDark]: "🙎🏾‍♀ī¸", + [.dark]: "🙎đŸŋ‍♀ī¸", + ] + case .noGood: + return [ + [.light]: "🙅đŸģ", + [.mediumLight]: "🙅đŸŧ", + [.medium]: "🙅đŸŊ", + [.mediumDark]: "🙅🏾", + [.dark]: "🙅đŸŋ", + ] + case .manGesturingNo: + return [ + [.light]: "🙅đŸģ‍♂ī¸", + [.mediumLight]: "🙅đŸŧ‍♂ī¸", + [.medium]: "🙅đŸŊ‍♂ī¸", + [.mediumDark]: "🙅🏾‍♂ī¸", + [.dark]: "🙅đŸŋ‍♂ī¸", + ] + case .womanGesturingNo: + return [ + [.light]: "🙅đŸģ‍♀ī¸", + [.mediumLight]: "🙅đŸŧ‍♀ī¸", + [.medium]: "🙅đŸŊ‍♀ī¸", + [.mediumDark]: "🙅🏾‍♀ī¸", + [.dark]: "🙅đŸŋ‍♀ī¸", + ] + case .okWoman: + return [ + [.light]: "🙆đŸģ", + [.mediumLight]: "🙆đŸŧ", + [.medium]: "🙆đŸŊ", + [.mediumDark]: "🙆🏾", + [.dark]: "🙆đŸŋ", + ] + case .manGesturingOk: + return [ + [.light]: "🙆đŸģ‍♂ī¸", + [.mediumLight]: "🙆đŸŧ‍♂ī¸", + [.medium]: "🙆đŸŊ‍♂ī¸", + [.mediumDark]: "🙆🏾‍♂ī¸", + [.dark]: "🙆đŸŋ‍♂ī¸", + ] + case .womanGesturingOk: + return [ + [.light]: "🙆đŸģ‍♀ī¸", + [.mediumLight]: "🙆đŸŧ‍♀ī¸", + [.medium]: "🙆đŸŊ‍♀ī¸", + [.mediumDark]: "🙆🏾‍♀ī¸", + [.dark]: "🙆đŸŋ‍♀ī¸", + ] + case .informationDeskPerson: + return [ + [.light]: "💁đŸģ", + [.mediumLight]: "💁đŸŧ", + [.medium]: "💁đŸŊ", + [.mediumDark]: "💁🏾", + [.dark]: "💁đŸŋ", + ] + case .manTippingHand: + return [ + [.light]: "💁đŸģ‍♂ī¸", + [.mediumLight]: "💁đŸŧ‍♂ī¸", + [.medium]: "💁đŸŊ‍♂ī¸", + [.mediumDark]: "💁🏾‍♂ī¸", + [.dark]: "💁đŸŋ‍♂ī¸", + ] + case .womanTippingHand: + return [ + [.light]: "💁đŸģ‍♀ī¸", + [.mediumLight]: "💁đŸŧ‍♀ī¸", + [.medium]: "💁đŸŊ‍♀ī¸", + [.mediumDark]: "💁🏾‍♀ī¸", + [.dark]: "💁đŸŋ‍♀ī¸", + ] + case .raisingHand: + return [ + [.light]: "🙋đŸģ", + [.mediumLight]: "🙋đŸŧ", + [.medium]: "🙋đŸŊ", + [.mediumDark]: "🙋🏾", + [.dark]: "🙋đŸŋ", + ] + case .manRaisingHand: + return [ + [.light]: "🙋đŸģ‍♂ī¸", + [.mediumLight]: "🙋đŸŧ‍♂ī¸", + [.medium]: "🙋đŸŊ‍♂ī¸", + [.mediumDark]: "🙋🏾‍♂ī¸", + [.dark]: "🙋đŸŋ‍♂ī¸", + ] + case .womanRaisingHand: + return [ + [.light]: "🙋đŸģ‍♀ī¸", + [.mediumLight]: "🙋đŸŧ‍♀ī¸", + [.medium]: "🙋đŸŊ‍♀ī¸", + [.mediumDark]: "🙋🏾‍♀ī¸", + [.dark]: "🙋đŸŋ‍♀ī¸", + ] + case .deafPerson: + return [ + [.light]: "🧏đŸģ", + [.mediumLight]: "🧏đŸŧ", + [.medium]: "🧏đŸŊ", + [.mediumDark]: "🧏🏾", + [.dark]: "🧏đŸŋ", + ] + case .deafMan: + return [ + [.light]: "🧏đŸģ‍♂ī¸", + [.mediumLight]: "🧏đŸŧ‍♂ī¸", + [.medium]: "🧏đŸŊ‍♂ī¸", + [.mediumDark]: "🧏🏾‍♂ī¸", + [.dark]: "🧏đŸŋ‍♂ī¸", + ] + case .deafWoman: + return [ + [.light]: "🧏đŸģ‍♀ī¸", + [.mediumLight]: "🧏đŸŧ‍♀ī¸", + [.medium]: "🧏đŸŊ‍♀ī¸", + [.mediumDark]: "🧏🏾‍♀ī¸", + [.dark]: "🧏đŸŋ‍♀ī¸", + ] + case .bow: + return [ + [.light]: "🙇đŸģ", + [.mediumLight]: "🙇đŸŧ", + [.medium]: "🙇đŸŊ", + [.mediumDark]: "🙇🏾", + [.dark]: "🙇đŸŋ", + ] + case .manBowing: + return [ + [.light]: "🙇đŸģ‍♂ī¸", + [.mediumLight]: "🙇đŸŧ‍♂ī¸", + [.medium]: "🙇đŸŊ‍♂ī¸", + [.mediumDark]: "🙇🏾‍♂ī¸", + [.dark]: "🙇đŸŋ‍♂ī¸", + ] + case .womanBowing: + return [ + [.light]: "🙇đŸģ‍♀ī¸", + [.mediumLight]: "🙇đŸŧ‍♀ī¸", + [.medium]: "🙇đŸŊ‍♀ī¸", + [.mediumDark]: "🙇🏾‍♀ī¸", + [.dark]: "🙇đŸŋ‍♀ī¸", + ] + case .facePalm: + return [ + [.light]: "đŸ¤ĻđŸģ", + [.mediumLight]: "đŸ¤ĻđŸŧ", + [.medium]: "đŸ¤ĻđŸŊ", + [.mediumDark]: "đŸ¤Ļ🏾", + [.dark]: "đŸ¤ĻđŸŋ", + ] + case .manFacepalming: + return [ + [.light]: "đŸ¤ĻđŸģ‍♂ī¸", + [.mediumLight]: "đŸ¤ĻđŸŧ‍♂ī¸", + [.medium]: "đŸ¤ĻđŸŊ‍♂ī¸", + [.mediumDark]: "đŸ¤Ļ🏾‍♂ī¸", + [.dark]: "đŸ¤ĻđŸŋ‍♂ī¸", + ] + case .womanFacepalming: + return [ + [.light]: "đŸ¤ĻđŸģ‍♀ī¸", + [.mediumLight]: "đŸ¤ĻđŸŧ‍♀ī¸", + [.medium]: "đŸ¤ĻđŸŊ‍♀ī¸", + [.mediumDark]: "đŸ¤Ļ🏾‍♀ī¸", + [.dark]: "đŸ¤ĻđŸŋ‍♀ī¸", + ] + case .shrug: + return [ + [.light]: "🤷đŸģ", + [.mediumLight]: "🤷đŸŧ", + [.medium]: "🤷đŸŊ", + [.mediumDark]: "🤷🏾", + [.dark]: "🤷đŸŋ", + ] + case .manShrugging: + return [ + [.light]: "🤷đŸģ‍♂ī¸", + [.mediumLight]: "🤷đŸŧ‍♂ī¸", + [.medium]: "🤷đŸŊ‍♂ī¸", + [.mediumDark]: "🤷🏾‍♂ī¸", + [.dark]: "🤷đŸŋ‍♂ī¸", + ] + case .womanShrugging: + return [ + [.light]: "🤷đŸģ‍♀ī¸", + [.mediumLight]: "🤷đŸŧ‍♀ī¸", + [.medium]: "🤷đŸŊ‍♀ī¸", + [.mediumDark]: "🤷🏾‍♀ī¸", + [.dark]: "🤷đŸŋ‍♀ī¸", + ] + case .healthWorker: + return [ + [.light]: "🧑đŸģ‍⚕ī¸", + [.mediumLight]: "🧑đŸŧ‍⚕ī¸", + [.medium]: "🧑đŸŊ‍⚕ī¸", + [.mediumDark]: "🧑🏾‍⚕ī¸", + [.dark]: "🧑đŸŋ‍⚕ī¸", + ] + case .maleDoctor: + return [ + [.light]: "👨đŸģ‍⚕ī¸", + [.mediumLight]: "👨đŸŧ‍⚕ī¸", + [.medium]: "👨đŸŊ‍⚕ī¸", + [.mediumDark]: "👨🏾‍⚕ī¸", + [.dark]: "👨đŸŋ‍⚕ī¸", + ] + case .femaleDoctor: + return [ + [.light]: "👩đŸģ‍⚕ī¸", + [.mediumLight]: "👩đŸŧ‍⚕ī¸", + [.medium]: "👩đŸŊ‍⚕ī¸", + [.mediumDark]: "👩🏾‍⚕ī¸", + [.dark]: "👩đŸŋ‍⚕ī¸", + ] + case .student: + return [ + [.light]: "🧑đŸģ‍🎓", + [.mediumLight]: "🧑đŸŧ‍🎓", + [.medium]: "🧑đŸŊ‍🎓", + [.mediumDark]: "🧑🏾‍🎓", + [.dark]: "🧑đŸŋ‍🎓", + ] + case .maleStudent: + return [ + [.light]: "👨đŸģ‍🎓", + [.mediumLight]: "👨đŸŧ‍🎓", + [.medium]: "👨đŸŊ‍🎓", + [.mediumDark]: "👨🏾‍🎓", + [.dark]: "👨đŸŋ‍🎓", + ] + case .femaleStudent: + return [ + [.light]: "👩đŸģ‍🎓", + [.mediumLight]: "👩đŸŧ‍🎓", + [.medium]: "👩đŸŊ‍🎓", + [.mediumDark]: "👩🏾‍🎓", + [.dark]: "👩đŸŋ‍🎓", + ] + case .teacher: + return [ + [.light]: "🧑đŸģ‍đŸĢ", + [.mediumLight]: "🧑đŸŧ‍đŸĢ", + [.medium]: "🧑đŸŊ‍đŸĢ", + [.mediumDark]: "🧑🏾‍đŸĢ", + [.dark]: "🧑đŸŋ‍đŸĢ", + ] + case .maleTeacher: + return [ + [.light]: "👨đŸģ‍đŸĢ", + [.mediumLight]: "👨đŸŧ‍đŸĢ", + [.medium]: "👨đŸŊ‍đŸĢ", + [.mediumDark]: "👨🏾‍đŸĢ", + [.dark]: "👨đŸŋ‍đŸĢ", + ] + case .femaleTeacher: + return [ + [.light]: "👩đŸģ‍đŸĢ", + [.mediumLight]: "👩đŸŧ‍đŸĢ", + [.medium]: "👩đŸŊ‍đŸĢ", + [.mediumDark]: "👩🏾‍đŸĢ", + [.dark]: "👩đŸŋ‍đŸĢ", + ] + case .judge: + return [ + [.light]: "🧑đŸģ‍⚖ī¸", + [.mediumLight]: "🧑đŸŧ‍⚖ī¸", + [.medium]: "🧑đŸŊ‍⚖ī¸", + [.mediumDark]: "🧑🏾‍⚖ī¸", + [.dark]: "🧑đŸŋ‍⚖ī¸", + ] + case .maleJudge: + return [ + [.light]: "👨đŸģ‍⚖ī¸", + [.mediumLight]: "👨đŸŧ‍⚖ī¸", + [.medium]: "👨đŸŊ‍⚖ī¸", + [.mediumDark]: "👨🏾‍⚖ī¸", + [.dark]: "👨đŸŋ‍⚖ī¸", + ] + case .femaleJudge: + return [ + [.light]: "👩đŸģ‍⚖ī¸", + [.mediumLight]: "👩đŸŧ‍⚖ī¸", + [.medium]: "👩đŸŊ‍⚖ī¸", + [.mediumDark]: "👩🏾‍⚖ī¸", + [.dark]: "👩đŸŋ‍⚖ī¸", + ] + case .farmer: + return [ + [.light]: "🧑đŸģ‍🌾", + [.mediumLight]: "🧑đŸŧ‍🌾", + [.medium]: "🧑đŸŊ‍🌾", + [.mediumDark]: "🧑🏾‍🌾", + [.dark]: "🧑đŸŋ‍🌾", + ] + case .maleFarmer: + return [ + [.light]: "👨đŸģ‍🌾", + [.mediumLight]: "👨đŸŧ‍🌾", + [.medium]: "👨đŸŊ‍🌾", + [.mediumDark]: "👨🏾‍🌾", + [.dark]: "👨đŸŋ‍🌾", + ] + case .femaleFarmer: + return [ + [.light]: "👩đŸģ‍🌾", + [.mediumLight]: "👩đŸŧ‍🌾", + [.medium]: "👩đŸŊ‍🌾", + [.mediumDark]: "👩🏾‍🌾", + [.dark]: "👩đŸŋ‍🌾", + ] + case .cook: + return [ + [.light]: "🧑đŸģ‍đŸŗ", + [.mediumLight]: "🧑đŸŧ‍đŸŗ", + [.medium]: "🧑đŸŊ‍đŸŗ", + [.mediumDark]: "🧑🏾‍đŸŗ", + [.dark]: "🧑đŸŋ‍đŸŗ", + ] + case .maleCook: + return [ + [.light]: "👨đŸģ‍đŸŗ", + [.mediumLight]: "👨đŸŧ‍đŸŗ", + [.medium]: "👨đŸŊ‍đŸŗ", + [.mediumDark]: "👨🏾‍đŸŗ", + [.dark]: "👨đŸŋ‍đŸŗ", + ] + case .femaleCook: + return [ + [.light]: "👩đŸģ‍đŸŗ", + [.mediumLight]: "👩đŸŧ‍đŸŗ", + [.medium]: "👩đŸŊ‍đŸŗ", + [.mediumDark]: "👩🏾‍đŸŗ", + [.dark]: "👩đŸŋ‍đŸŗ", + ] + case .mechanic: + return [ + [.light]: "🧑đŸģ‍🔧", + [.mediumLight]: "🧑đŸŧ‍🔧", + [.medium]: "🧑đŸŊ‍🔧", + [.mediumDark]: "🧑🏾‍🔧", + [.dark]: "🧑đŸŋ‍🔧", + ] + case .maleMechanic: + return [ + [.light]: "👨đŸģ‍🔧", + [.mediumLight]: "👨đŸŧ‍🔧", + [.medium]: "👨đŸŊ‍🔧", + [.mediumDark]: "👨🏾‍🔧", + [.dark]: "👨đŸŋ‍🔧", + ] + case .femaleMechanic: + return [ + [.light]: "👩đŸģ‍🔧", + [.mediumLight]: "👩đŸŧ‍🔧", + [.medium]: "👩đŸŊ‍🔧", + [.mediumDark]: "👩🏾‍🔧", + [.dark]: "👩đŸŋ‍🔧", + ] + case .factoryWorker: + return [ + [.light]: "🧑đŸģ‍🏭", + [.mediumLight]: "🧑đŸŧ‍🏭", + [.medium]: "🧑đŸŊ‍🏭", + [.mediumDark]: "🧑🏾‍🏭", + [.dark]: "🧑đŸŋ‍🏭", + ] + case .maleFactoryWorker: + return [ + [.light]: "👨đŸģ‍🏭", + [.mediumLight]: "👨đŸŧ‍🏭", + [.medium]: "👨đŸŊ‍🏭", + [.mediumDark]: "👨🏾‍🏭", + [.dark]: "👨đŸŋ‍🏭", + ] + case .femaleFactoryWorker: + return [ + [.light]: "👩đŸģ‍🏭", + [.mediumLight]: "👩đŸŧ‍🏭", + [.medium]: "👩đŸŊ‍🏭", + [.mediumDark]: "👩🏾‍🏭", + [.dark]: "👩đŸŋ‍🏭", + ] + case .officeWorker: + return [ + [.light]: "🧑đŸģ‍đŸ’ŧ", + [.mediumLight]: "🧑đŸŧ‍đŸ’ŧ", + [.medium]: "🧑đŸŊ‍đŸ’ŧ", + [.mediumDark]: "🧑🏾‍đŸ’ŧ", + [.dark]: "🧑đŸŋ‍đŸ’ŧ", + ] + case .maleOfficeWorker: + return [ + [.light]: "👨đŸģ‍đŸ’ŧ", + [.mediumLight]: "👨đŸŧ‍đŸ’ŧ", + [.medium]: "👨đŸŊ‍đŸ’ŧ", + [.mediumDark]: "👨🏾‍đŸ’ŧ", + [.dark]: "👨đŸŋ‍đŸ’ŧ", + ] + case .femaleOfficeWorker: + return [ + [.light]: "👩đŸģ‍đŸ’ŧ", + [.mediumLight]: "👩đŸŧ‍đŸ’ŧ", + [.medium]: "👩đŸŊ‍đŸ’ŧ", + [.mediumDark]: "👩🏾‍đŸ’ŧ", + [.dark]: "👩đŸŋ‍đŸ’ŧ", + ] + case .scientist: + return [ + [.light]: "🧑đŸģ‍đŸ”Ŧ", + [.mediumLight]: "🧑đŸŧ‍đŸ”Ŧ", + [.medium]: "🧑đŸŊ‍đŸ”Ŧ", + [.mediumDark]: "🧑🏾‍đŸ”Ŧ", + [.dark]: "🧑đŸŋ‍đŸ”Ŧ", + ] + case .maleScientist: + return [ + [.light]: "👨đŸģ‍đŸ”Ŧ", + [.mediumLight]: "👨đŸŧ‍đŸ”Ŧ", + [.medium]: "👨đŸŊ‍đŸ”Ŧ", + [.mediumDark]: "👨🏾‍đŸ”Ŧ", + [.dark]: "👨đŸŋ‍đŸ”Ŧ", + ] + case .femaleScientist: + return [ + [.light]: "👩đŸģ‍đŸ”Ŧ", + [.mediumLight]: "👩đŸŧ‍đŸ”Ŧ", + [.medium]: "👩đŸŊ‍đŸ”Ŧ", + [.mediumDark]: "👩🏾‍đŸ”Ŧ", + [.dark]: "👩đŸŋ‍đŸ”Ŧ", + ] + case .technologist: + return [ + [.light]: "🧑đŸģ‍đŸ’ģ", + [.mediumLight]: "🧑đŸŧ‍đŸ’ģ", + [.medium]: "🧑đŸŊ‍đŸ’ģ", + [.mediumDark]: "🧑🏾‍đŸ’ģ", + [.dark]: "🧑đŸŋ‍đŸ’ģ", + ] + case .maleTechnologist: + return [ + [.light]: "👨đŸģ‍đŸ’ģ", + [.mediumLight]: "👨đŸŧ‍đŸ’ģ", + [.medium]: "👨đŸŊ‍đŸ’ģ", + [.mediumDark]: "👨🏾‍đŸ’ģ", + [.dark]: "👨đŸŋ‍đŸ’ģ", + ] + case .femaleTechnologist: + return [ + [.light]: "👩đŸģ‍đŸ’ģ", + [.mediumLight]: "👩đŸŧ‍đŸ’ģ", + [.medium]: "👩đŸŊ‍đŸ’ģ", + [.mediumDark]: "👩🏾‍đŸ’ģ", + [.dark]: "👩đŸŋ‍đŸ’ģ", + ] + case .singer: + return [ + [.light]: "🧑đŸģ‍🎤", + [.mediumLight]: "🧑đŸŧ‍🎤", + [.medium]: "🧑đŸŊ‍🎤", + [.mediumDark]: "🧑🏾‍🎤", + [.dark]: "🧑đŸŋ‍🎤", + ] + case .maleSinger: + return [ + [.light]: "👨đŸģ‍🎤", + [.mediumLight]: "👨đŸŧ‍🎤", + [.medium]: "👨đŸŊ‍🎤", + [.mediumDark]: "👨🏾‍🎤", + [.dark]: "👨đŸŋ‍🎤", + ] + case .femaleSinger: + return [ + [.light]: "👩đŸģ‍🎤", + [.mediumLight]: "👩đŸŧ‍🎤", + [.medium]: "👩đŸŊ‍🎤", + [.mediumDark]: "👩🏾‍🎤", + [.dark]: "👩đŸŋ‍🎤", + ] + case .artist: + return [ + [.light]: "🧑đŸģ‍🎨", + [.mediumLight]: "🧑đŸŧ‍🎨", + [.medium]: "🧑đŸŊ‍🎨", + [.mediumDark]: "🧑🏾‍🎨", + [.dark]: "🧑đŸŋ‍🎨", + ] + case .maleArtist: + return [ + [.light]: "👨đŸģ‍🎨", + [.mediumLight]: "👨đŸŧ‍🎨", + [.medium]: "👨đŸŊ‍🎨", + [.mediumDark]: "👨🏾‍🎨", + [.dark]: "👨đŸŋ‍🎨", + ] + case .femaleArtist: + return [ + [.light]: "👩đŸģ‍🎨", + [.mediumLight]: "👩đŸŧ‍🎨", + [.medium]: "👩đŸŊ‍🎨", + [.mediumDark]: "👩🏾‍🎨", + [.dark]: "👩đŸŋ‍🎨", + ] + case .pilot: + return [ + [.light]: "🧑đŸģ‍✈ī¸", + [.mediumLight]: "🧑đŸŧ‍✈ī¸", + [.medium]: "🧑đŸŊ‍✈ī¸", + [.mediumDark]: "🧑🏾‍✈ī¸", + [.dark]: "🧑đŸŋ‍✈ī¸", + ] + case .malePilot: + return [ + [.light]: "👨đŸģ‍✈ī¸", + [.mediumLight]: "👨đŸŧ‍✈ī¸", + [.medium]: "👨đŸŊ‍✈ī¸", + [.mediumDark]: "👨🏾‍✈ī¸", + [.dark]: "👨đŸŋ‍✈ī¸", + ] + case .femalePilot: + return [ + [.light]: "👩đŸģ‍✈ī¸", + [.mediumLight]: "👩đŸŧ‍✈ī¸", + [.medium]: "👩đŸŊ‍✈ī¸", + [.mediumDark]: "👩🏾‍✈ī¸", + [.dark]: "👩đŸŋ‍✈ī¸", + ] + case .astronaut: + return [ + [.light]: "🧑đŸģ‍🚀", + [.mediumLight]: "🧑đŸŧ‍🚀", + [.medium]: "🧑đŸŊ‍🚀", + [.mediumDark]: "🧑🏾‍🚀", + [.dark]: "🧑đŸŋ‍🚀", + ] + case .maleAstronaut: + return [ + [.light]: "👨đŸģ‍🚀", + [.mediumLight]: "👨đŸŧ‍🚀", + [.medium]: "👨đŸŊ‍🚀", + [.mediumDark]: "👨🏾‍🚀", + [.dark]: "👨đŸŋ‍🚀", + ] + case .femaleAstronaut: + return [ + [.light]: "👩đŸģ‍🚀", + [.mediumLight]: "👩đŸŧ‍🚀", + [.medium]: "👩đŸŊ‍🚀", + [.mediumDark]: "👩🏾‍🚀", + [.dark]: "👩đŸŋ‍🚀", + ] + case .firefighter: + return [ + [.light]: "🧑đŸģ‍🚒", + [.mediumLight]: "🧑đŸŧ‍🚒", + [.medium]: "🧑đŸŊ‍🚒", + [.mediumDark]: "🧑🏾‍🚒", + [.dark]: "🧑đŸŋ‍🚒", + ] + case .maleFirefighter: + return [ + [.light]: "👨đŸģ‍🚒", + [.mediumLight]: "👨đŸŧ‍🚒", + [.medium]: "👨đŸŊ‍🚒", + [.mediumDark]: "👨🏾‍🚒", + [.dark]: "👨đŸŋ‍🚒", + ] + case .femaleFirefighter: + return [ + [.light]: "👩đŸģ‍🚒", + [.mediumLight]: "👩đŸŧ‍🚒", + [.medium]: "👩đŸŊ‍🚒", + [.mediumDark]: "👩🏾‍🚒", + [.dark]: "👩đŸŋ‍🚒", + ] + case .cop: + return [ + [.light]: "👮đŸģ", + [.mediumLight]: "👮đŸŧ", + [.medium]: "👮đŸŊ", + [.mediumDark]: "👮🏾", + [.dark]: "👮đŸŋ", + ] + case .malePoliceOfficer: + return [ + [.light]: "👮đŸģ‍♂ī¸", + [.mediumLight]: "👮đŸŧ‍♂ī¸", + [.medium]: "👮đŸŊ‍♂ī¸", + [.mediumDark]: "👮🏾‍♂ī¸", + [.dark]: "👮đŸŋ‍♂ī¸", + ] + case .femalePoliceOfficer: + return [ + [.light]: "👮đŸģ‍♀ī¸", + [.mediumLight]: "👮đŸŧ‍♀ī¸", + [.medium]: "👮đŸŊ‍♀ī¸", + [.mediumDark]: "👮🏾‍♀ī¸", + [.dark]: "👮đŸŋ‍♀ī¸", + ] + case .sleuthOrSpy: + return [ + [.light]: "đŸ•ĩđŸģ", + [.mediumLight]: "đŸ•ĩđŸŧ", + [.medium]: "đŸ•ĩđŸŊ", + [.mediumDark]: "đŸ•ĩ🏾", + [.dark]: "đŸ•ĩđŸŋ", + ] + case .maleDetective: + return [ + [.light]: "đŸ•ĩđŸģ‍♂ī¸", + [.mediumLight]: "đŸ•ĩđŸŧ‍♂ī¸", + [.medium]: "đŸ•ĩđŸŊ‍♂ī¸", + [.mediumDark]: "đŸ•ĩ🏾‍♂ī¸", + [.dark]: "đŸ•ĩđŸŋ‍♂ī¸", + ] + case .femaleDetective: + return [ + [.light]: "đŸ•ĩđŸģ‍♀ī¸", + [.mediumLight]: "đŸ•ĩđŸŧ‍♀ī¸", + [.medium]: "đŸ•ĩđŸŊ‍♀ī¸", + [.mediumDark]: "đŸ•ĩ🏾‍♀ī¸", + [.dark]: "đŸ•ĩđŸŋ‍♀ī¸", + ] + case .guardsman: + return [ + [.light]: "💂đŸģ", + [.mediumLight]: "💂đŸŧ", + [.medium]: "💂đŸŊ", + [.mediumDark]: "💂🏾", + [.dark]: "💂đŸŋ", + ] + case .maleGuard: + return [ + [.light]: "💂đŸģ‍♂ī¸", + [.mediumLight]: "💂đŸŧ‍♂ī¸", + [.medium]: "💂đŸŊ‍♂ī¸", + [.mediumDark]: "💂🏾‍♂ī¸", + [.dark]: "💂đŸŋ‍♂ī¸", + ] + case .femaleGuard: + return [ + [.light]: "💂đŸģ‍♀ī¸", + [.mediumLight]: "💂đŸŧ‍♀ī¸", + [.medium]: "💂đŸŊ‍♀ī¸", + [.mediumDark]: "💂🏾‍♀ī¸", + [.dark]: "💂đŸŋ‍♀ī¸", + ] + case .ninja: + return [ + [.light]: "đŸĨˇđŸģ", + [.mediumLight]: "đŸĨˇđŸŧ", + [.medium]: "đŸĨˇđŸŊ", + [.mediumDark]: "đŸĨˇđŸž", + [.dark]: "đŸĨˇđŸŋ", + ] + case .constructionWorker: + return [ + [.light]: "👷đŸģ", + [.mediumLight]: "👷đŸŧ", + [.medium]: "👷đŸŊ", + [.mediumDark]: "👷🏾", + [.dark]: "👷đŸŋ", + ] + case .maleConstructionWorker: + return [ + [.light]: "👷đŸģ‍♂ī¸", + [.mediumLight]: "👷đŸŧ‍♂ī¸", + [.medium]: "👷đŸŊ‍♂ī¸", + [.mediumDark]: "👷🏾‍♂ī¸", + [.dark]: "👷đŸŋ‍♂ī¸", + ] + case .femaleConstructionWorker: + return [ + [.light]: "👷đŸģ‍♀ī¸", + [.mediumLight]: "👷đŸŧ‍♀ī¸", + [.medium]: "👷đŸŊ‍♀ī¸", + [.mediumDark]: "👷🏾‍♀ī¸", + [.dark]: "👷đŸŋ‍♀ī¸", + ] + case .personWithCrown: + return [ + [.light]: "đŸĢ…đŸģ", + [.mediumLight]: "đŸĢ…đŸŧ", + [.medium]: "đŸĢ…đŸŊ", + [.mediumDark]: "đŸĢ…đŸž", + [.dark]: "đŸĢ…đŸŋ", + ] + case .prince: + return [ + [.light]: "🤴đŸģ", + [.mediumLight]: "🤴đŸŧ", + [.medium]: "🤴đŸŊ", + [.mediumDark]: "🤴🏾", + [.dark]: "🤴đŸŋ", + ] + case .princess: + return [ + [.light]: "👸đŸģ", + [.mediumLight]: "👸đŸŧ", + [.medium]: "👸đŸŊ", + [.mediumDark]: "👸🏾", + [.dark]: "👸đŸŋ", + ] + case .manWithTurban: + return [ + [.light]: "đŸ‘ŗđŸģ", + [.mediumLight]: "đŸ‘ŗđŸŧ", + [.medium]: "đŸ‘ŗđŸŊ", + [.mediumDark]: "đŸ‘ŗ🏾", + [.dark]: "đŸ‘ŗđŸŋ", + ] + case .manWearingTurban: + return [ + [.light]: "đŸ‘ŗđŸģ‍♂ī¸", + [.mediumLight]: "đŸ‘ŗđŸŧ‍♂ī¸", + [.medium]: "đŸ‘ŗđŸŊ‍♂ī¸", + [.mediumDark]: "đŸ‘ŗ🏾‍♂ī¸", + [.dark]: "đŸ‘ŗđŸŋ‍♂ī¸", + ] + case .womanWearingTurban: + return [ + [.light]: "đŸ‘ŗđŸģ‍♀ī¸", + [.mediumLight]: "đŸ‘ŗđŸŧ‍♀ī¸", + [.medium]: "đŸ‘ŗđŸŊ‍♀ī¸", + [.mediumDark]: "đŸ‘ŗ🏾‍♀ī¸", + [.dark]: "đŸ‘ŗđŸŋ‍♀ī¸", + ] + case .manWithGuaPiMao: + return [ + [.light]: "👲đŸģ", + [.mediumLight]: "👲đŸŧ", + [.medium]: "👲đŸŊ", + [.mediumDark]: "👲🏾", + [.dark]: "👲đŸŋ", + ] + case .personWithHeadscarf: + return [ + [.light]: "🧕đŸģ", + [.mediumLight]: "🧕đŸŧ", + [.medium]: "🧕đŸŊ", + [.mediumDark]: "🧕🏾", + [.dark]: "🧕đŸŋ", + ] + case .personInTuxedo: + return [ + [.light]: "đŸ¤ĩđŸģ", + [.mediumLight]: "đŸ¤ĩđŸŧ", + [.medium]: "đŸ¤ĩđŸŊ", + [.mediumDark]: "đŸ¤ĩ🏾", + [.dark]: "đŸ¤ĩđŸŋ", + ] + case .manInTuxedo: + return [ + [.light]: "đŸ¤ĩđŸģ‍♂ī¸", + [.mediumLight]: "đŸ¤ĩđŸŧ‍♂ī¸", + [.medium]: "đŸ¤ĩđŸŊ‍♂ī¸", + [.mediumDark]: "đŸ¤ĩ🏾‍♂ī¸", + [.dark]: "đŸ¤ĩđŸŋ‍♂ī¸", + ] + case .womanInTuxedo: + return [ + [.light]: "đŸ¤ĩđŸģ‍♀ī¸", + [.mediumLight]: "đŸ¤ĩđŸŧ‍♀ī¸", + [.medium]: "đŸ¤ĩđŸŊ‍♀ī¸", + [.mediumDark]: "đŸ¤ĩ🏾‍♀ī¸", + [.dark]: "đŸ¤ĩđŸŋ‍♀ī¸", + ] + case .brideWithVeil: + return [ + [.light]: "👰đŸģ", + [.mediumLight]: "👰đŸŧ", + [.medium]: "👰đŸŊ", + [.mediumDark]: "👰🏾", + [.dark]: "👰đŸŋ", + ] + case .manWithVeil: + return [ + [.light]: "👰đŸģ‍♂ī¸", + [.mediumLight]: "👰đŸŧ‍♂ī¸", + [.medium]: "👰đŸŊ‍♂ī¸", + [.mediumDark]: "👰🏾‍♂ī¸", + [.dark]: "👰đŸŋ‍♂ī¸", + ] + case .womanWithVeil: + return [ + [.light]: "👰đŸģ‍♀ī¸", + [.mediumLight]: "👰đŸŧ‍♀ī¸", + [.medium]: "👰đŸŊ‍♀ī¸", + [.mediumDark]: "👰🏾‍♀ī¸", + [.dark]: "👰đŸŋ‍♀ī¸", + ] + case .pregnantWoman: + return [ + [.light]: "🤰đŸģ", + [.mediumLight]: "🤰đŸŧ", + [.medium]: "🤰đŸŊ", + [.mediumDark]: "🤰🏾", + [.dark]: "🤰đŸŋ", + ] + case .pregnantMan: + return [ + [.light]: "đŸĢƒđŸģ", + [.mediumLight]: "đŸĢƒđŸŧ", + [.medium]: "đŸĢƒđŸŊ", + [.mediumDark]: "đŸĢƒđŸž", + [.dark]: "đŸĢƒđŸŋ", + ] + case .pregnantPerson: + return [ + [.light]: "đŸĢ„đŸģ", + [.mediumLight]: "đŸĢ„đŸŧ", + [.medium]: "đŸĢ„đŸŊ", + [.mediumDark]: "đŸĢ„đŸž", + [.dark]: "đŸĢ„đŸŋ", + ] + case .breastFeeding: + return [ + [.light]: "🤱đŸģ", + [.mediumLight]: "🤱đŸŧ", + [.medium]: "🤱đŸŊ", + [.mediumDark]: "🤱🏾", + [.dark]: "🤱đŸŋ", + ] + case .womanFeedingBaby: + return [ + [.light]: "👩đŸģ‍đŸŧ", + [.mediumLight]: "👩đŸŧ‍đŸŧ", + [.medium]: "👩đŸŊ‍đŸŧ", + [.mediumDark]: "👩🏾‍đŸŧ", + [.dark]: "👩đŸŋ‍đŸŧ", + ] + case .manFeedingBaby: + return [ + [.light]: "👨đŸģ‍đŸŧ", + [.mediumLight]: "👨đŸŧ‍đŸŧ", + [.medium]: "👨đŸŊ‍đŸŧ", + [.mediumDark]: "👨🏾‍đŸŧ", + [.dark]: "👨đŸŋ‍đŸŧ", + ] + case .personFeedingBaby: + return [ + [.light]: "🧑đŸģ‍đŸŧ", + [.mediumLight]: "🧑đŸŧ‍đŸŧ", + [.medium]: "🧑đŸŊ‍đŸŧ", + [.mediumDark]: "🧑🏾‍đŸŧ", + [.dark]: "🧑đŸŋ‍đŸŧ", + ] + case .angel: + return [ + [.light]: "đŸ‘ŧđŸģ", + [.mediumLight]: "đŸ‘ŧđŸŧ", + [.medium]: "đŸ‘ŧđŸŊ", + [.mediumDark]: "đŸ‘ŧ🏾", + [.dark]: "đŸ‘ŧđŸŋ", + ] + case .santa: + return [ + [.light]: "🎅đŸģ", + [.mediumLight]: "🎅đŸŧ", + [.medium]: "🎅đŸŊ", + [.mediumDark]: "🎅🏾", + [.dark]: "🎅đŸŋ", + ] + case .mrsClaus: + return [ + [.light]: "đŸ¤ļđŸģ", + [.mediumLight]: "đŸ¤ļđŸŧ", + [.medium]: "đŸ¤ļđŸŊ", + [.mediumDark]: "đŸ¤ļ🏾", + [.dark]: "đŸ¤ļđŸŋ", + ] + case .mxClaus: + return [ + [.light]: "🧑đŸģ‍🎄", + [.mediumLight]: "🧑đŸŧ‍🎄", + [.medium]: "🧑đŸŊ‍🎄", + [.mediumDark]: "🧑🏾‍🎄", + [.dark]: "🧑đŸŋ‍🎄", + ] + case .superhero: + return [ + [.light]: "đŸĻ¸đŸģ", + [.mediumLight]: "đŸĻ¸đŸŧ", + [.medium]: "đŸĻ¸đŸŊ", + [.mediumDark]: "đŸĻ¸đŸž", + [.dark]: "đŸĻ¸đŸŋ", + ] + case .maleSuperhero: + return [ + [.light]: "đŸĻ¸đŸģ‍♂ī¸", + [.mediumLight]: "đŸĻ¸đŸŧ‍♂ī¸", + [.medium]: "đŸĻ¸đŸŊ‍♂ī¸", + [.mediumDark]: "đŸĻ¸đŸžâ€â™‚ī¸", + [.dark]: "đŸĻ¸đŸŋ‍♂ī¸", + ] + case .femaleSuperhero: + return [ + [.light]: "đŸĻ¸đŸģ‍♀ī¸", + [.mediumLight]: "đŸĻ¸đŸŧ‍♀ī¸", + [.medium]: "đŸĻ¸đŸŊ‍♀ī¸", + [.mediumDark]: "đŸĻ¸đŸžâ€â™€ī¸", + [.dark]: "đŸĻ¸đŸŋ‍♀ī¸", + ] + case .supervillain: + return [ + [.light]: "đŸĻšđŸģ", + [.mediumLight]: "đŸĻšđŸŧ", + [.medium]: "đŸĻšđŸŊ", + [.mediumDark]: "đŸĻšđŸž", + [.dark]: "đŸĻšđŸŋ", + ] + case .maleSupervillain: + return [ + [.light]: "đŸĻšđŸģ‍♂ī¸", + [.mediumLight]: "đŸĻšđŸŧ‍♂ī¸", + [.medium]: "đŸĻšđŸŊ‍♂ī¸", + [.mediumDark]: "đŸĻšđŸžâ€â™‚ī¸", + [.dark]: "đŸĻšđŸŋ‍♂ī¸", + ] + case .femaleSupervillain: + return [ + [.light]: "đŸĻšđŸģ‍♀ī¸", + [.mediumLight]: "đŸĻšđŸŧ‍♀ī¸", + [.medium]: "đŸĻšđŸŊ‍♀ī¸", + [.mediumDark]: "đŸĻšđŸžâ€â™€ī¸", + [.dark]: "đŸĻšđŸŋ‍♀ī¸", + ] + case .mage: + return [ + [.light]: "🧙đŸģ", + [.mediumLight]: "🧙đŸŧ", + [.medium]: "🧙đŸŊ", + [.mediumDark]: "🧙🏾", + [.dark]: "🧙đŸŋ", + ] + case .maleMage: + return [ + [.light]: "🧙đŸģ‍♂ī¸", + [.mediumLight]: "🧙đŸŧ‍♂ī¸", + [.medium]: "🧙đŸŊ‍♂ī¸", + [.mediumDark]: "🧙🏾‍♂ī¸", + [.dark]: "🧙đŸŋ‍♂ī¸", + ] + case .femaleMage: + return [ + [.light]: "🧙đŸģ‍♀ī¸", + [.mediumLight]: "🧙đŸŧ‍♀ī¸", + [.medium]: "🧙đŸŊ‍♀ī¸", + [.mediumDark]: "🧙🏾‍♀ī¸", + [.dark]: "🧙đŸŋ‍♀ī¸", + ] + case .fairy: + return [ + [.light]: "🧚đŸģ", + [.mediumLight]: "🧚đŸŧ", + [.medium]: "🧚đŸŊ", + [.mediumDark]: "🧚🏾", + [.dark]: "🧚đŸŋ", + ] + case .maleFairy: + return [ + [.light]: "🧚đŸģ‍♂ī¸", + [.mediumLight]: "🧚đŸŧ‍♂ī¸", + [.medium]: "🧚đŸŊ‍♂ī¸", + [.mediumDark]: "🧚🏾‍♂ī¸", + [.dark]: "🧚đŸŋ‍♂ī¸", + ] + case .femaleFairy: + return [ + [.light]: "🧚đŸģ‍♀ī¸", + [.mediumLight]: "🧚đŸŧ‍♀ī¸", + [.medium]: "🧚đŸŊ‍♀ī¸", + [.mediumDark]: "🧚🏾‍♀ī¸", + [.dark]: "🧚đŸŋ‍♀ī¸", + ] + case .vampire: + return [ + [.light]: "🧛đŸģ", + [.mediumLight]: "🧛đŸŧ", + [.medium]: "🧛đŸŊ", + [.mediumDark]: "🧛🏾", + [.dark]: "🧛đŸŋ", + ] + case .maleVampire: + return [ + [.light]: "🧛đŸģ‍♂ī¸", + [.mediumLight]: "🧛đŸŧ‍♂ī¸", + [.medium]: "🧛đŸŊ‍♂ī¸", + [.mediumDark]: "🧛🏾‍♂ī¸", + [.dark]: "🧛đŸŋ‍♂ī¸", + ] + case .femaleVampire: + return [ + [.light]: "🧛đŸģ‍♀ī¸", + [.mediumLight]: "🧛đŸŧ‍♀ī¸", + [.medium]: "🧛đŸŊ‍♀ī¸", + [.mediumDark]: "🧛🏾‍♀ī¸", + [.dark]: "🧛đŸŋ‍♀ī¸", + ] + case .merperson: + return [ + [.light]: "🧜đŸģ", + [.mediumLight]: "🧜đŸŧ", + [.medium]: "🧜đŸŊ", + [.mediumDark]: "🧜🏾", + [.dark]: "🧜đŸŋ", + ] + case .merman: + return [ + [.light]: "🧜đŸģ‍♂ī¸", + [.mediumLight]: "🧜đŸŧ‍♂ī¸", + [.medium]: "🧜đŸŊ‍♂ī¸", + [.mediumDark]: "🧜🏾‍♂ī¸", + [.dark]: "🧜đŸŋ‍♂ī¸", + ] + case .mermaid: + return [ + [.light]: "🧜đŸģ‍♀ī¸", + [.mediumLight]: "🧜đŸŧ‍♀ī¸", + [.medium]: "🧜đŸŊ‍♀ī¸", + [.mediumDark]: "🧜🏾‍♀ī¸", + [.dark]: "🧜đŸŋ‍♀ī¸", + ] + case .elf: + return [ + [.light]: "🧝đŸģ", + [.mediumLight]: "🧝đŸŧ", + [.medium]: "🧝đŸŊ", + [.mediumDark]: "🧝🏾", + [.dark]: "🧝đŸŋ", + ] + case .maleElf: + return [ + [.light]: "🧝đŸģ‍♂ī¸", + [.mediumLight]: "🧝đŸŧ‍♂ī¸", + [.medium]: "🧝đŸŊ‍♂ī¸", + [.mediumDark]: "🧝🏾‍♂ī¸", + [.dark]: "🧝đŸŋ‍♂ī¸", + ] + case .femaleElf: + return [ + [.light]: "🧝đŸģ‍♀ī¸", + [.mediumLight]: "🧝đŸŧ‍♀ī¸", + [.medium]: "🧝đŸŊ‍♀ī¸", + [.mediumDark]: "🧝🏾‍♀ī¸", + [.dark]: "🧝đŸŋ‍♀ī¸", + ] + case .massage: + return [ + [.light]: "💆đŸģ", + [.mediumLight]: "💆đŸŧ", + [.medium]: "💆đŸŊ", + [.mediumDark]: "💆🏾", + [.dark]: "💆đŸŋ", + ] + case .manGettingMassage: + return [ + [.light]: "💆đŸģ‍♂ī¸", + [.mediumLight]: "💆đŸŧ‍♂ī¸", + [.medium]: "💆đŸŊ‍♂ī¸", + [.mediumDark]: "💆🏾‍♂ī¸", + [.dark]: "💆đŸŋ‍♂ī¸", + ] + case .womanGettingMassage: + return [ + [.light]: "💆đŸģ‍♀ī¸", + [.mediumLight]: "💆đŸŧ‍♀ī¸", + [.medium]: "💆đŸŊ‍♀ī¸", + [.mediumDark]: "💆🏾‍♀ī¸", + [.dark]: "💆đŸŋ‍♀ī¸", + ] + case .haircut: + return [ + [.light]: "💇đŸģ", + [.mediumLight]: "💇đŸŧ", + [.medium]: "💇đŸŊ", + [.mediumDark]: "💇🏾", + [.dark]: "💇đŸŋ", + ] + case .manGettingHaircut: + return [ + [.light]: "💇đŸģ‍♂ī¸", + [.mediumLight]: "💇đŸŧ‍♂ī¸", + [.medium]: "💇đŸŊ‍♂ī¸", + [.mediumDark]: "💇🏾‍♂ī¸", + [.dark]: "💇đŸŋ‍♂ī¸", + ] + case .womanGettingHaircut: + return [ + [.light]: "💇đŸģ‍♀ī¸", + [.mediumLight]: "💇đŸŧ‍♀ī¸", + [.medium]: "💇đŸŊ‍♀ī¸", + [.mediumDark]: "💇🏾‍♀ī¸", + [.dark]: "💇đŸŋ‍♀ī¸", + ] + case .walking: + return [ + [.light]: "đŸšļđŸģ", + [.mediumLight]: "đŸšļđŸŧ", + [.medium]: "đŸšļđŸŊ", + [.mediumDark]: "đŸšļ🏾", + [.dark]: "đŸšļđŸŋ", + ] + case .manWalking: + return [ + [.light]: "đŸšļđŸģ‍♂ī¸", + [.mediumLight]: "đŸšļđŸŧ‍♂ī¸", + [.medium]: "đŸšļđŸŊ‍♂ī¸", + [.mediumDark]: "đŸšļ🏾‍♂ī¸", + [.dark]: "đŸšļđŸŋ‍♂ī¸", + ] + case .womanWalking: + return [ + [.light]: "đŸšļđŸģ‍♀ī¸", + [.mediumLight]: "đŸšļđŸŧ‍♀ī¸", + [.medium]: "đŸšļđŸŊ‍♀ī¸", + [.mediumDark]: "đŸšļ🏾‍♀ī¸", + [.dark]: "đŸšļđŸŋ‍♀ī¸", + ] + case .standingPerson: + return [ + [.light]: "🧍đŸģ", + [.mediumLight]: "🧍đŸŧ", + [.medium]: "🧍đŸŊ", + [.mediumDark]: "🧍🏾", + [.dark]: "🧍đŸŋ", + ] + case .manStanding: + return [ + [.light]: "🧍đŸģ‍♂ī¸", + [.mediumLight]: "🧍đŸŧ‍♂ī¸", + [.medium]: "🧍đŸŊ‍♂ī¸", + [.mediumDark]: "🧍🏾‍♂ī¸", + [.dark]: "🧍đŸŋ‍♂ī¸", + ] + case .womanStanding: + return [ + [.light]: "🧍đŸģ‍♀ī¸", + [.mediumLight]: "🧍đŸŧ‍♀ī¸", + [.medium]: "🧍đŸŊ‍♀ī¸", + [.mediumDark]: "🧍🏾‍♀ī¸", + [.dark]: "🧍đŸŋ‍♀ī¸", + ] + case .kneelingPerson: + return [ + [.light]: "🧎đŸģ", + [.mediumLight]: "🧎đŸŧ", + [.medium]: "🧎đŸŊ", + [.mediumDark]: "🧎🏾", + [.dark]: "🧎đŸŋ", + ] + case .manKneeling: + return [ + [.light]: "🧎đŸģ‍♂ī¸", + [.mediumLight]: "🧎đŸŧ‍♂ī¸", + [.medium]: "🧎đŸŊ‍♂ī¸", + [.mediumDark]: "🧎🏾‍♂ī¸", + [.dark]: "🧎đŸŋ‍♂ī¸", + ] + case .womanKneeling: + return [ + [.light]: "🧎đŸģ‍♀ī¸", + [.mediumLight]: "🧎đŸŧ‍♀ī¸", + [.medium]: "🧎đŸŊ‍♀ī¸", + [.mediumDark]: "🧎🏾‍♀ī¸", + [.dark]: "🧎đŸŋ‍♀ī¸", + ] + case .personWithProbingCane: + return [ + [.light]: "🧑đŸģ‍đŸĻ¯", + [.mediumLight]: "🧑đŸŧ‍đŸĻ¯", + [.medium]: "🧑đŸŊ‍đŸĻ¯", + [.mediumDark]: "🧑🏾‍đŸĻ¯", + [.dark]: "🧑đŸŋ‍đŸĻ¯", + ] + case .manWithProbingCane: + return [ + [.light]: "👨đŸģ‍đŸĻ¯", + [.mediumLight]: "👨đŸŧ‍đŸĻ¯", + [.medium]: "👨đŸŊ‍đŸĻ¯", + [.mediumDark]: "👨🏾‍đŸĻ¯", + [.dark]: "👨đŸŋ‍đŸĻ¯", + ] + case .womanWithProbingCane: + return [ + [.light]: "👩đŸģ‍đŸĻ¯", + [.mediumLight]: "👩đŸŧ‍đŸĻ¯", + [.medium]: "👩đŸŊ‍đŸĻ¯", + [.mediumDark]: "👩🏾‍đŸĻ¯", + [.dark]: "👩đŸŋ‍đŸĻ¯", + ] + case .personInMotorizedWheelchair: + return [ + [.light]: "🧑đŸģ‍đŸĻŧ", + [.mediumLight]: "🧑đŸŧ‍đŸĻŧ", + [.medium]: "🧑đŸŊ‍đŸĻŧ", + [.mediumDark]: "🧑🏾‍đŸĻŧ", + [.dark]: "🧑đŸŋ‍đŸĻŧ", + ] + case .manInMotorizedWheelchair: + return [ + [.light]: "👨đŸģ‍đŸĻŧ", + [.mediumLight]: "👨đŸŧ‍đŸĻŧ", + [.medium]: "👨đŸŊ‍đŸĻŧ", + [.mediumDark]: "👨🏾‍đŸĻŧ", + [.dark]: "👨đŸŋ‍đŸĻŧ", + ] + case .womanInMotorizedWheelchair: + return [ + [.light]: "👩đŸģ‍đŸĻŧ", + [.mediumLight]: "👩đŸŧ‍đŸĻŧ", + [.medium]: "👩đŸŊ‍đŸĻŧ", + [.mediumDark]: "👩🏾‍đŸĻŧ", + [.dark]: "👩đŸŋ‍đŸĻŧ", + ] + case .personInManualWheelchair: + return [ + [.light]: "🧑đŸģ‍đŸĻŊ", + [.mediumLight]: "🧑đŸŧ‍đŸĻŊ", + [.medium]: "🧑đŸŊ‍đŸĻŊ", + [.mediumDark]: "🧑🏾‍đŸĻŊ", + [.dark]: "🧑đŸŋ‍đŸĻŊ", + ] + case .manInManualWheelchair: + return [ + [.light]: "👨đŸģ‍đŸĻŊ", + [.mediumLight]: "👨đŸŧ‍đŸĻŊ", + [.medium]: "👨đŸŊ‍đŸĻŊ", + [.mediumDark]: "👨🏾‍đŸĻŊ", + [.dark]: "👨đŸŋ‍đŸĻŊ", + ] + case .womanInManualWheelchair: + return [ + [.light]: "👩đŸģ‍đŸĻŊ", + [.mediumLight]: "👩đŸŧ‍đŸĻŊ", + [.medium]: "👩đŸŊ‍đŸĻŊ", + [.mediumDark]: "👩🏾‍đŸĻŊ", + [.dark]: "👩đŸŋ‍đŸĻŊ", + ] + case .runner: + return [ + [.light]: "🏃đŸģ", + [.mediumLight]: "🏃đŸŧ", + [.medium]: "🏃đŸŊ", + [.mediumDark]: "🏃🏾", + [.dark]: "🏃đŸŋ", + ] + case .manRunning: + return [ + [.light]: "🏃đŸģ‍♂ī¸", + [.mediumLight]: "🏃đŸŧ‍♂ī¸", + [.medium]: "🏃đŸŊ‍♂ī¸", + [.mediumDark]: "🏃🏾‍♂ī¸", + [.dark]: "🏃đŸŋ‍♂ī¸", + ] + case .womanRunning: + return [ + [.light]: "🏃đŸģ‍♀ī¸", + [.mediumLight]: "🏃đŸŧ‍♀ī¸", + [.medium]: "🏃đŸŊ‍♀ī¸", + [.mediumDark]: "🏃🏾‍♀ī¸", + [.dark]: "🏃đŸŋ‍♀ī¸", + ] + case .dancer: + return [ + [.light]: "💃đŸģ", + [.mediumLight]: "💃đŸŧ", + [.medium]: "💃đŸŊ", + [.mediumDark]: "💃🏾", + [.dark]: "💃đŸŋ", + ] + case .manDancing: + return [ + [.light]: "đŸ•ēđŸģ", + [.mediumLight]: "đŸ•ēđŸŧ", + [.medium]: "đŸ•ēđŸŊ", + [.mediumDark]: "đŸ•ē🏾", + [.dark]: "đŸ•ēđŸŋ", + ] + case .manInBusinessSuitLevitating: + return [ + [.light]: "🕴đŸģ", + [.mediumLight]: "🕴đŸŧ", + [.medium]: "🕴đŸŊ", + [.mediumDark]: "🕴🏾", + [.dark]: "🕴đŸŋ", + ] + case .personInSteamyRoom: + return [ + [.light]: "🧖đŸģ", + [.mediumLight]: "🧖đŸŧ", + [.medium]: "🧖đŸŊ", + [.mediumDark]: "🧖🏾", + [.dark]: "🧖đŸŋ", + ] + case .manInSteamyRoom: + return [ + [.light]: "🧖đŸģ‍♂ī¸", + [.mediumLight]: "🧖đŸŧ‍♂ī¸", + [.medium]: "🧖đŸŊ‍♂ī¸", + [.mediumDark]: "🧖🏾‍♂ī¸", + [.dark]: "🧖đŸŋ‍♂ī¸", + ] + case .womanInSteamyRoom: + return [ + [.light]: "🧖đŸģ‍♀ī¸", + [.mediumLight]: "🧖đŸŧ‍♀ī¸", + [.medium]: "🧖đŸŊ‍♀ī¸", + [.mediumDark]: "🧖🏾‍♀ī¸", + [.dark]: "🧖đŸŋ‍♀ī¸", + ] + case .personClimbing: + return [ + [.light]: "🧗đŸģ", + [.mediumLight]: "🧗đŸŧ", + [.medium]: "🧗đŸŊ", + [.mediumDark]: "🧗🏾", + [.dark]: "🧗đŸŋ", + ] + case .manClimbing: + return [ + [.light]: "🧗đŸģ‍♂ī¸", + [.mediumLight]: "🧗đŸŧ‍♂ī¸", + [.medium]: "🧗đŸŊ‍♂ī¸", + [.mediumDark]: "🧗🏾‍♂ī¸", + [.dark]: "🧗đŸŋ‍♂ī¸", + ] + case .womanClimbing: + return [ + [.light]: "🧗đŸģ‍♀ī¸", + [.mediumLight]: "🧗đŸŧ‍♀ī¸", + [.medium]: "🧗đŸŊ‍♀ī¸", + [.mediumDark]: "🧗🏾‍♀ī¸", + [.dark]: "🧗đŸŋ‍♀ī¸", + ] + case .horseRacing: + return [ + [.light]: "🏇đŸģ", + [.mediumLight]: "🏇đŸŧ", + [.medium]: "🏇đŸŊ", + [.mediumDark]: "🏇🏾", + [.dark]: "🏇đŸŋ", + ] + case .snowboarder: + return [ + [.light]: "🏂đŸģ", + [.mediumLight]: "🏂đŸŧ", + [.medium]: "🏂đŸŊ", + [.mediumDark]: "🏂🏾", + [.dark]: "🏂đŸŋ", + ] + case .golfer: + return [ + [.light]: "🏌đŸģ", + [.mediumLight]: "🏌đŸŧ", + [.medium]: "🏌đŸŊ", + [.mediumDark]: "🏌🏾", + [.dark]: "🏌đŸŋ", + ] + case .manGolfing: + return [ + [.light]: "🏌đŸģ‍♂ī¸", + [.mediumLight]: "🏌đŸŧ‍♂ī¸", + [.medium]: "🏌đŸŊ‍♂ī¸", + [.mediumDark]: "🏌🏾‍♂ī¸", + [.dark]: "🏌đŸŋ‍♂ī¸", + ] + case .womanGolfing: + return [ + [.light]: "🏌đŸģ‍♀ī¸", + [.mediumLight]: "🏌đŸŧ‍♀ī¸", + [.medium]: "🏌đŸŊ‍♀ī¸", + [.mediumDark]: "🏌🏾‍♀ī¸", + [.dark]: "🏌đŸŋ‍♀ī¸", + ] + case .surfer: + return [ + [.light]: "🏄đŸģ", + [.mediumLight]: "🏄đŸŧ", + [.medium]: "🏄đŸŊ", + [.mediumDark]: "🏄🏾", + [.dark]: "🏄đŸŋ", + ] + case .manSurfing: + return [ + [.light]: "🏄đŸģ‍♂ī¸", + [.mediumLight]: "🏄đŸŧ‍♂ī¸", + [.medium]: "🏄đŸŊ‍♂ī¸", + [.mediumDark]: "🏄🏾‍♂ī¸", + [.dark]: "🏄đŸŋ‍♂ī¸", + ] + case .womanSurfing: + return [ + [.light]: "🏄đŸģ‍♀ī¸", + [.mediumLight]: "🏄đŸŧ‍♀ī¸", + [.medium]: "🏄đŸŊ‍♀ī¸", + [.mediumDark]: "🏄🏾‍♀ī¸", + [.dark]: "🏄đŸŋ‍♀ī¸", + ] + case .rowboat: + return [ + [.light]: "đŸšŖđŸģ", + [.mediumLight]: "đŸšŖđŸŧ", + [.medium]: "đŸšŖđŸŊ", + [.mediumDark]: "đŸšŖ🏾", + [.dark]: "đŸšŖđŸŋ", + ] + case .manRowingBoat: + return [ + [.light]: "đŸšŖđŸģ‍♂ī¸", + [.mediumLight]: "đŸšŖđŸŧ‍♂ī¸", + [.medium]: "đŸšŖđŸŊ‍♂ī¸", + [.mediumDark]: "đŸšŖ🏾‍♂ī¸", + [.dark]: "đŸšŖđŸŋ‍♂ī¸", + ] + case .womanRowingBoat: + return [ + [.light]: "đŸšŖđŸģ‍♀ī¸", + [.mediumLight]: "đŸšŖđŸŧ‍♀ī¸", + [.medium]: "đŸšŖđŸŊ‍♀ī¸", + [.mediumDark]: "đŸšŖ🏾‍♀ī¸", + [.dark]: "đŸšŖđŸŋ‍♀ī¸", + ] + case .swimmer: + return [ + [.light]: "🏊đŸģ", + [.mediumLight]: "🏊đŸŧ", + [.medium]: "🏊đŸŊ", + [.mediumDark]: "🏊🏾", + [.dark]: "🏊đŸŋ", + ] + case .manSwimming: + return [ + [.light]: "🏊đŸģ‍♂ī¸", + [.mediumLight]: "🏊đŸŧ‍♂ī¸", + [.medium]: "🏊đŸŊ‍♂ī¸", + [.mediumDark]: "🏊🏾‍♂ī¸", + [.dark]: "🏊đŸŋ‍♂ī¸", + ] + case .womanSwimming: + return [ + [.light]: "🏊đŸģ‍♀ī¸", + [.mediumLight]: "🏊đŸŧ‍♀ī¸", + [.medium]: "🏊đŸŊ‍♀ī¸", + [.mediumDark]: "🏊🏾‍♀ī¸", + [.dark]: "🏊đŸŋ‍♀ī¸", + ] + case .personWithBall: + return [ + [.light]: "⛹đŸģ", + [.mediumLight]: "⛹đŸŧ", + [.medium]: "⛹đŸŊ", + [.mediumDark]: "⛹🏾", + [.dark]: "⛹đŸŋ", + ] + case .manBouncingBall: + return [ + [.light]: "⛹đŸģ‍♂ī¸", + [.mediumLight]: "⛹đŸŧ‍♂ī¸", + [.medium]: "⛹đŸŊ‍♂ī¸", + [.mediumDark]: "⛹🏾‍♂ī¸", + [.dark]: "⛹đŸŋ‍♂ī¸", + ] + case .womanBouncingBall: + return [ + [.light]: "⛹đŸģ‍♀ī¸", + [.mediumLight]: "⛹đŸŧ‍♀ī¸", + [.medium]: "⛹đŸŊ‍♀ī¸", + [.mediumDark]: "⛹🏾‍♀ī¸", + [.dark]: "⛹đŸŋ‍♀ī¸", + ] + case .weightLifter: + return [ + [.light]: "🏋đŸģ", + [.mediumLight]: "🏋đŸŧ", + [.medium]: "🏋đŸŊ", + [.mediumDark]: "🏋🏾", + [.dark]: "🏋đŸŋ", + ] + case .manLiftingWeights: + return [ + [.light]: "🏋đŸģ‍♂ī¸", + [.mediumLight]: "🏋đŸŧ‍♂ī¸", + [.medium]: "🏋đŸŊ‍♂ī¸", + [.mediumDark]: "🏋🏾‍♂ī¸", + [.dark]: "🏋đŸŋ‍♂ī¸", + ] + case .womanLiftingWeights: + return [ + [.light]: "🏋đŸģ‍♀ī¸", + [.mediumLight]: "🏋đŸŧ‍♀ī¸", + [.medium]: "🏋đŸŊ‍♀ī¸", + [.mediumDark]: "🏋🏾‍♀ī¸", + [.dark]: "🏋đŸŋ‍♀ī¸", + ] + case .bicyclist: + return [ + [.light]: "🚴đŸģ", + [.mediumLight]: "🚴đŸŧ", + [.medium]: "🚴đŸŊ", + [.mediumDark]: "🚴🏾", + [.dark]: "🚴đŸŋ", + ] + case .manBiking: + return [ + [.light]: "🚴đŸģ‍♂ī¸", + [.mediumLight]: "🚴đŸŧ‍♂ī¸", + [.medium]: "🚴đŸŊ‍♂ī¸", + [.mediumDark]: "🚴🏾‍♂ī¸", + [.dark]: "🚴đŸŋ‍♂ī¸", + ] + case .womanBiking: + return [ + [.light]: "🚴đŸģ‍♀ī¸", + [.mediumLight]: "🚴đŸŧ‍♀ī¸", + [.medium]: "🚴đŸŊ‍♀ī¸", + [.mediumDark]: "🚴🏾‍♀ī¸", + [.dark]: "🚴đŸŋ‍♀ī¸", + ] + case .mountainBicyclist: + return [ + [.light]: "đŸšĩđŸģ", + [.mediumLight]: "đŸšĩđŸŧ", + [.medium]: "đŸšĩđŸŊ", + [.mediumDark]: "đŸšĩ🏾", + [.dark]: "đŸšĩđŸŋ", + ] + case .manMountainBiking: + return [ + [.light]: "đŸšĩđŸģ‍♂ī¸", + [.mediumLight]: "đŸšĩđŸŧ‍♂ī¸", + [.medium]: "đŸšĩđŸŊ‍♂ī¸", + [.mediumDark]: "đŸšĩ🏾‍♂ī¸", + [.dark]: "đŸšĩđŸŋ‍♂ī¸", + ] + case .womanMountainBiking: + return [ + [.light]: "đŸšĩđŸģ‍♀ī¸", + [.mediumLight]: "đŸšĩđŸŧ‍♀ī¸", + [.medium]: "đŸšĩđŸŊ‍♀ī¸", + [.mediumDark]: "đŸšĩ🏾‍♀ī¸", + [.dark]: "đŸšĩđŸŋ‍♀ī¸", + ] + case .personDoingCartwheel: + return [ + [.light]: "🤸đŸģ", + [.mediumLight]: "🤸đŸŧ", + [.medium]: "🤸đŸŊ", + [.mediumDark]: "🤸🏾", + [.dark]: "🤸đŸŋ", + ] + case .manCartwheeling: + return [ + [.light]: "🤸đŸģ‍♂ī¸", + [.mediumLight]: "🤸đŸŧ‍♂ī¸", + [.medium]: "🤸đŸŊ‍♂ī¸", + [.mediumDark]: "🤸🏾‍♂ī¸", + [.dark]: "🤸đŸŋ‍♂ī¸", + ] + case .womanCartwheeling: + return [ + [.light]: "🤸đŸģ‍♀ī¸", + [.mediumLight]: "🤸đŸŧ‍♀ī¸", + [.medium]: "🤸đŸŊ‍♀ī¸", + [.mediumDark]: "🤸🏾‍♀ī¸", + [.dark]: "🤸đŸŋ‍♀ī¸", + ] + case .waterPolo: + return [ + [.light]: "đŸ¤ŊđŸģ", + [.mediumLight]: "đŸ¤ŊđŸŧ", + [.medium]: "đŸ¤ŊđŸŊ", + [.mediumDark]: "đŸ¤Ŋ🏾", + [.dark]: "đŸ¤ŊđŸŋ", + ] + case .manPlayingWaterPolo: + return [ + [.light]: "đŸ¤ŊđŸģ‍♂ī¸", + [.mediumLight]: "đŸ¤ŊđŸŧ‍♂ī¸", + [.medium]: "đŸ¤ŊđŸŊ‍♂ī¸", + [.mediumDark]: "đŸ¤Ŋ🏾‍♂ī¸", + [.dark]: "đŸ¤ŊđŸŋ‍♂ī¸", + ] + case .womanPlayingWaterPolo: + return [ + [.light]: "đŸ¤ŊđŸģ‍♀ī¸", + [.mediumLight]: "đŸ¤ŊđŸŧ‍♀ī¸", + [.medium]: "đŸ¤ŊđŸŊ‍♀ī¸", + [.mediumDark]: "đŸ¤Ŋ🏾‍♀ī¸", + [.dark]: "đŸ¤ŊđŸŋ‍♀ī¸", + ] + case .handball: + return [ + [.light]: "🤾đŸģ", + [.mediumLight]: "🤾đŸŧ", + [.medium]: "🤾đŸŊ", + [.mediumDark]: "🤾🏾", + [.dark]: "🤾đŸŋ", + ] + case .manPlayingHandball: + return [ + [.light]: "🤾đŸģ‍♂ī¸", + [.mediumLight]: "🤾đŸŧ‍♂ī¸", + [.medium]: "🤾đŸŊ‍♂ī¸", + [.mediumDark]: "🤾🏾‍♂ī¸", + [.dark]: "🤾đŸŋ‍♂ī¸", + ] + case .womanPlayingHandball: + return [ + [.light]: "🤾đŸģ‍♀ī¸", + [.mediumLight]: "🤾đŸŧ‍♀ī¸", + [.medium]: "🤾đŸŊ‍♀ī¸", + [.mediumDark]: "🤾🏾‍♀ī¸", + [.dark]: "🤾đŸŋ‍♀ī¸", + ] + case .juggling: + return [ + [.light]: "🤹đŸģ", + [.mediumLight]: "🤹đŸŧ", + [.medium]: "🤹đŸŊ", + [.mediumDark]: "🤹🏾", + [.dark]: "🤹đŸŋ", + ] + case .manJuggling: + return [ + [.light]: "🤹đŸģ‍♂ī¸", + [.mediumLight]: "🤹đŸŧ‍♂ī¸", + [.medium]: "🤹đŸŊ‍♂ī¸", + [.mediumDark]: "🤹🏾‍♂ī¸", + [.dark]: "🤹đŸŋ‍♂ī¸", + ] + case .womanJuggling: + return [ + [.light]: "🤹đŸģ‍♀ī¸", + [.mediumLight]: "🤹đŸŧ‍♀ī¸", + [.medium]: "🤹đŸŊ‍♀ī¸", + [.mediumDark]: "🤹🏾‍♀ī¸", + [.dark]: "🤹đŸŋ‍♀ī¸", + ] + case .personInLotusPosition: + return [ + [.light]: "🧘đŸģ", + [.mediumLight]: "🧘đŸŧ", + [.medium]: "🧘đŸŊ", + [.mediumDark]: "🧘🏾", + [.dark]: "🧘đŸŋ", + ] + case .manInLotusPosition: + return [ + [.light]: "🧘đŸģ‍♂ī¸", + [.mediumLight]: "🧘đŸŧ‍♂ī¸", + [.medium]: "🧘đŸŊ‍♂ī¸", + [.mediumDark]: "🧘🏾‍♂ī¸", + [.dark]: "🧘đŸŋ‍♂ī¸", + ] + case .womanInLotusPosition: + return [ + [.light]: "🧘đŸģ‍♀ī¸", + [.mediumLight]: "🧘đŸŧ‍♀ī¸", + [.medium]: "🧘đŸŊ‍♀ī¸", + [.mediumDark]: "🧘🏾‍♀ī¸", + [.dark]: "🧘đŸŋ‍♀ī¸", + ] + case .bath: + return [ + [.light]: "🛀đŸģ", + [.mediumLight]: "🛀đŸŧ", + [.medium]: "🛀đŸŊ", + [.mediumDark]: "🛀🏾", + [.dark]: "🛀đŸŋ", + ] + case .sleepingAccommodation: + return [ + [.light]: "🛌đŸģ", + [.mediumLight]: "🛌đŸŧ", + [.medium]: "🛌đŸŊ", + [.mediumDark]: "🛌🏾", + [.dark]: "🛌đŸŋ", + ] + case .peopleHoldingHands: + return [ + [.light]: "🧑đŸģ‍🤝‍🧑đŸģ", + [.light, .mediumLight]: "🧑đŸģ‍🤝‍🧑đŸŧ", + [.light, .medium]: "🧑đŸģ‍🤝‍🧑đŸŊ", + [.light, .mediumDark]: "🧑đŸģ‍🤝‍🧑🏾", + [.light, .dark]: "🧑đŸģ‍🤝‍🧑đŸŋ", + [.mediumLight]: "🧑đŸŧ‍🤝‍🧑đŸŧ", + [.mediumLight, .light]: "🧑đŸŧ‍🤝‍🧑đŸģ", + [.mediumLight, .medium]: "🧑đŸŧ‍🤝‍🧑đŸŊ", + [.mediumLight, .mediumDark]: "🧑đŸŧ‍🤝‍🧑🏾", + [.mediumLight, .dark]: "🧑đŸŧ‍🤝‍🧑đŸŋ", + [.medium]: "🧑đŸŊ‍🤝‍🧑đŸŊ", + [.medium, .light]: "🧑đŸŊ‍🤝‍🧑đŸģ", + [.medium, .mediumLight]: "🧑đŸŊ‍🤝‍🧑đŸŧ", + [.medium, .mediumDark]: "🧑đŸŊ‍🤝‍🧑🏾", + [.medium, .dark]: "🧑đŸŊ‍🤝‍🧑đŸŋ", + [.mediumDark]: "🧑🏾‍🤝‍🧑🏾", + [.mediumDark, .light]: "🧑🏾‍🤝‍🧑đŸģ", + [.mediumDark, .mediumLight]: "🧑🏾‍🤝‍🧑đŸŧ", + [.mediumDark, .medium]: "🧑🏾‍🤝‍🧑đŸŊ", + [.mediumDark, .dark]: "🧑🏾‍🤝‍🧑đŸŋ", + [.dark]: "🧑đŸŋ‍🤝‍🧑đŸŋ", + [.dark, .light]: "🧑đŸŋ‍🤝‍🧑đŸģ", + [.dark, .mediumLight]: "🧑đŸŋ‍🤝‍🧑đŸŧ", + [.dark, .medium]: "🧑đŸŋ‍🤝‍🧑đŸŊ", + [.dark, .mediumDark]: "🧑đŸŋ‍🤝‍🧑🏾", + ] + case .twoWomenHoldingHands: + return [ + [.light]: "👭đŸģ", + [.light, .mediumLight]: "👩đŸģ‍🤝‍👩đŸŧ", + [.light, .medium]: "👩đŸģ‍🤝‍👩đŸŊ", + [.light, .mediumDark]: "👩đŸģ‍🤝‍👩🏾", + [.light, .dark]: "👩đŸģ‍🤝‍👩đŸŋ", + [.mediumLight]: "👭đŸŧ", + [.mediumLight, .light]: "👩đŸŧ‍🤝‍👩đŸģ", + [.mediumLight, .medium]: "👩đŸŧ‍🤝‍👩đŸŊ", + [.mediumLight, .mediumDark]: "👩đŸŧ‍🤝‍👩🏾", + [.mediumLight, .dark]: "👩đŸŧ‍🤝‍👩đŸŋ", + [.medium]: "👭đŸŊ", + [.medium, .light]: "👩đŸŊ‍🤝‍👩đŸģ", + [.medium, .mediumLight]: "👩đŸŊ‍🤝‍👩đŸŧ", + [.medium, .mediumDark]: "👩đŸŊ‍🤝‍👩🏾", + [.medium, .dark]: "👩đŸŊ‍🤝‍👩đŸŋ", + [.mediumDark]: "👭🏾", + [.mediumDark, .light]: "👩🏾‍🤝‍👩đŸģ", + [.mediumDark, .mediumLight]: "👩🏾‍🤝‍👩đŸŧ", + [.mediumDark, .medium]: "👩🏾‍🤝‍👩đŸŊ", + [.mediumDark, .dark]: "👩🏾‍🤝‍👩đŸŋ", + [.dark]: "👭đŸŋ", + [.dark, .light]: "👩đŸŋ‍🤝‍👩đŸģ", + [.dark, .mediumLight]: "👩đŸŋ‍🤝‍👩đŸŧ", + [.dark, .medium]: "👩đŸŋ‍🤝‍👩đŸŊ", + [.dark, .mediumDark]: "👩đŸŋ‍🤝‍👩🏾", + ] + case .manAndWomanHoldingHands: + return [ + [.light]: "đŸ‘ĢđŸģ", + [.light, .mediumLight]: "👩đŸģ‍🤝‍👨đŸŧ", + [.light, .medium]: "👩đŸģ‍🤝‍👨đŸŊ", + [.light, .mediumDark]: "👩đŸģ‍🤝‍👨🏾", + [.light, .dark]: "👩đŸģ‍🤝‍👨đŸŋ", + [.mediumLight]: "đŸ‘ĢđŸŧ", + [.mediumLight, .light]: "👩đŸŧ‍🤝‍👨đŸģ", + [.mediumLight, .medium]: "👩đŸŧ‍🤝‍👨đŸŊ", + [.mediumLight, .mediumDark]: "👩đŸŧ‍🤝‍👨🏾", + [.mediumLight, .dark]: "👩đŸŧ‍🤝‍👨đŸŋ", + [.medium]: "đŸ‘ĢđŸŊ", + [.medium, .light]: "👩đŸŊ‍🤝‍👨đŸģ", + [.medium, .mediumLight]: "👩đŸŊ‍🤝‍👨đŸŧ", + [.medium, .mediumDark]: "👩đŸŊ‍🤝‍👨🏾", + [.medium, .dark]: "👩đŸŊ‍🤝‍👨đŸŋ", + [.mediumDark]: "đŸ‘Ģ🏾", + [.mediumDark, .light]: "👩🏾‍🤝‍👨đŸģ", + [.mediumDark, .mediumLight]: "👩🏾‍🤝‍👨đŸŧ", + [.mediumDark, .medium]: "👩🏾‍🤝‍👨đŸŊ", + [.mediumDark, .dark]: "👩🏾‍🤝‍👨đŸŋ", + [.dark]: "đŸ‘ĢđŸŋ", + [.dark, .light]: "👩đŸŋ‍🤝‍👨đŸģ", + [.dark, .mediumLight]: "👩đŸŋ‍🤝‍👨đŸŧ", + [.dark, .medium]: "👩đŸŋ‍🤝‍👨đŸŊ", + [.dark, .mediumDark]: "👩đŸŋ‍🤝‍👨🏾", + ] + case .twoMenHoldingHands: + return [ + [.light]: "đŸ‘ŦđŸģ", + [.light, .mediumLight]: "👨đŸģ‍🤝‍👨đŸŧ", + [.light, .medium]: "👨đŸģ‍🤝‍👨đŸŊ", + [.light, .mediumDark]: "👨đŸģ‍🤝‍👨🏾", + [.light, .dark]: "👨đŸģ‍🤝‍👨đŸŋ", + [.mediumLight]: "đŸ‘ŦđŸŧ", + [.mediumLight, .light]: "👨đŸŧ‍🤝‍👨đŸģ", + [.mediumLight, .medium]: "👨đŸŧ‍🤝‍👨đŸŊ", + [.mediumLight, .mediumDark]: "👨đŸŧ‍🤝‍👨🏾", + [.mediumLight, .dark]: "👨đŸŧ‍🤝‍👨đŸŋ", + [.medium]: "đŸ‘ŦđŸŊ", + [.medium, .light]: "👨đŸŊ‍🤝‍👨đŸģ", + [.medium, .mediumLight]: "👨đŸŊ‍🤝‍👨đŸŧ", + [.medium, .mediumDark]: "👨đŸŊ‍🤝‍👨🏾", + [.medium, .dark]: "👨đŸŊ‍🤝‍👨đŸŋ", + [.mediumDark]: "đŸ‘Ŧ🏾", + [.mediumDark, .light]: "👨🏾‍🤝‍👨đŸģ", + [.mediumDark, .mediumLight]: "👨🏾‍🤝‍👨đŸŧ", + [.mediumDark, .medium]: "👨🏾‍🤝‍👨đŸŊ", + [.mediumDark, .dark]: "👨🏾‍🤝‍👨đŸŋ", + [.dark]: "đŸ‘ŦđŸŋ", + [.dark, .light]: "👨đŸŋ‍🤝‍👨đŸģ", + [.dark, .mediumLight]: "👨đŸŋ‍🤝‍👨đŸŧ", + [.dark, .medium]: "👨đŸŋ‍🤝‍👨đŸŊ", + [.dark, .mediumDark]: "👨đŸŋ‍🤝‍👨🏾", + ] + case .personKissPerson: + return [ + [.light]: "💏đŸģ", + [.light, .mediumLight]: "🧑đŸģ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŧ", + [.light, .medium]: "🧑đŸģ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŊ", + [.light, .mediumDark]: "🧑đŸģ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸž", + [.light, .dark]: "🧑đŸģ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŋ", + [.mediumLight]: "💏đŸŧ", + [.mediumLight, .light]: "🧑đŸŧ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸģ", + [.mediumLight, .medium]: "🧑đŸŧ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŊ", + [.mediumLight, .mediumDark]: "🧑đŸŧ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸž", + [.mediumLight, .dark]: "🧑đŸŧ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŋ", + [.medium]: "💏đŸŊ", + [.medium, .light]: "🧑đŸŊ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸģ", + [.medium, .mediumLight]: "🧑đŸŊ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŧ", + [.medium, .mediumDark]: "🧑đŸŊ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸž", + [.medium, .dark]: "🧑đŸŊ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŋ", + [.mediumDark]: "💏🏾", + [.mediumDark, .light]: "🧑🏾‍❤ī¸â€đŸ’‹â€đŸ§‘đŸģ", + [.mediumDark, .mediumLight]: "🧑🏾‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŧ", + [.mediumDark, .medium]: "🧑🏾‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŊ", + [.mediumDark, .dark]: "🧑🏾‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŋ", + [.dark]: "💏đŸŋ", + [.dark, .light]: "🧑đŸŋ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸģ", + [.dark, .mediumLight]: "🧑đŸŋ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŧ", + [.dark, .medium]: "🧑đŸŋ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŊ", + [.dark, .mediumDark]: "🧑đŸŋ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸž", + ] + case .womanKissMan: + return [ + [.light]: "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ", + [.light, .mediumLight]: "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ", + [.light, .medium]: "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ", + [.light, .mediumDark]: "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž", + [.light, .dark]: "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ", + [.mediumLight]: "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ", + [.mediumLight, .light]: "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ", + [.mediumLight, .medium]: "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ", + [.mediumLight, .mediumDark]: "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž", + [.mediumLight, .dark]: "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ", + [.medium]: "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ", + [.medium, .light]: "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ", + [.medium, .mediumLight]: "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ", + [.medium, .mediumDark]: "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž", + [.medium, .dark]: "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ", + [.mediumDark]: "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž", + [.mediumDark, .light]: "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ", + [.mediumDark, .mediumLight]: "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ", + [.mediumDark, .medium]: "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ", + [.mediumDark, .dark]: "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ", + [.dark]: "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ", + [.dark, .light]: "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ", + [.dark, .mediumLight]: "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ", + [.dark, .medium]: "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ", + [.dark, .mediumDark]: "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž", + ] + case .manKissMan: + return [ + [.light]: "👨đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ", + [.light, .mediumLight]: "👨đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ", + [.light, .medium]: "👨đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ", + [.light, .mediumDark]: "👨đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž", + [.light, .dark]: "👨đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ", + [.mediumLight]: "👨đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ", + [.mediumLight, .light]: "👨đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ", + [.mediumLight, .medium]: "👨đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ", + [.mediumLight, .mediumDark]: "👨đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž", + [.mediumLight, .dark]: "👨đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ", + [.medium]: "👨đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ", + [.medium, .light]: "👨đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ", + [.medium, .mediumLight]: "👨đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ", + [.medium, .mediumDark]: "👨đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž", + [.medium, .dark]: "👨đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ", + [.mediumDark]: "👨🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž", + [.mediumDark, .light]: "👨🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ", + [.mediumDark, .mediumLight]: "👨🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ", + [.mediumDark, .medium]: "👨🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ", + [.mediumDark, .dark]: "👨🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ", + [.dark]: "👨đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ", + [.dark, .light]: "👨đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ", + [.dark, .mediumLight]: "👨đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ", + [.dark, .medium]: "👨đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ", + [.dark, .mediumDark]: "👨đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž", + ] + case .womanKissWoman: + return [ + [.light]: "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸģ", + [.light, .mediumLight]: "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŧ", + [.light, .medium]: "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŊ", + [.light, .mediumDark]: "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸž", + [.light, .dark]: "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŋ", + [.mediumLight]: "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŧ", + [.mediumLight, .light]: "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸģ", + [.mediumLight, .medium]: "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŊ", + [.mediumLight, .mediumDark]: "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸž", + [.mediumLight, .dark]: "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŋ", + [.medium]: "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŊ", + [.medium, .light]: "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸģ", + [.medium, .mediumLight]: "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŧ", + [.medium, .mediumDark]: "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸž", + [.medium, .dark]: "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŋ", + [.mediumDark]: "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸž", + [.mediumDark, .light]: "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸģ", + [.mediumDark, .mediumLight]: "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŧ", + [.mediumDark, .medium]: "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŊ", + [.mediumDark, .dark]: "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŋ", + [.dark]: "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŋ", + [.dark, .light]: "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸģ", + [.dark, .mediumLight]: "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŧ", + [.dark, .medium]: "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŊ", + [.dark, .mediumDark]: "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸž", + ] + case .personHeartPerson: + return [ + [.light]: "💑đŸģ", + [.light, .mediumLight]: "🧑đŸģ‍❤ī¸â€đŸ§‘đŸŧ", + [.light, .medium]: "🧑đŸģ‍❤ī¸â€đŸ§‘đŸŊ", + [.light, .mediumDark]: "🧑đŸģ‍❤ī¸â€đŸ§‘đŸž", + [.light, .dark]: "🧑đŸģ‍❤ī¸â€đŸ§‘đŸŋ", + [.mediumLight]: "💑đŸŧ", + [.mediumLight, .light]: "🧑đŸŧ‍❤ī¸â€đŸ§‘đŸģ", + [.mediumLight, .medium]: "🧑đŸŧ‍❤ī¸â€đŸ§‘đŸŊ", + [.mediumLight, .mediumDark]: "🧑đŸŧ‍❤ī¸â€đŸ§‘đŸž", + [.mediumLight, .dark]: "🧑đŸŧ‍❤ī¸â€đŸ§‘đŸŋ", + [.medium]: "💑đŸŊ", + [.medium, .light]: "🧑đŸŊ‍❤ī¸â€đŸ§‘đŸģ", + [.medium, .mediumLight]: "🧑đŸŊ‍❤ī¸â€đŸ§‘đŸŧ", + [.medium, .mediumDark]: "🧑đŸŊ‍❤ī¸â€đŸ§‘đŸž", + [.medium, .dark]: "🧑đŸŊ‍❤ī¸â€đŸ§‘đŸŋ", + [.mediumDark]: "💑🏾", + [.mediumDark, .light]: "🧑🏾‍❤ī¸â€đŸ§‘đŸģ", + [.mediumDark, .mediumLight]: "🧑🏾‍❤ī¸â€đŸ§‘đŸŧ", + [.mediumDark, .medium]: "🧑🏾‍❤ī¸â€đŸ§‘đŸŊ", + [.mediumDark, .dark]: "🧑🏾‍❤ī¸â€đŸ§‘đŸŋ", + [.dark]: "💑đŸŋ", + [.dark, .light]: "🧑đŸŋ‍❤ī¸â€đŸ§‘đŸģ", + [.dark, .mediumLight]: "🧑đŸŋ‍❤ī¸â€đŸ§‘đŸŧ", + [.dark, .medium]: "🧑đŸŋ‍❤ī¸â€đŸ§‘đŸŊ", + [.dark, .mediumDark]: "🧑đŸŋ‍❤ī¸â€đŸ§‘đŸž", + ] + case .womanHeartMan: + return [ + [.light]: "👩đŸģ‍❤ī¸â€đŸ‘¨đŸģ", + [.light, .mediumLight]: "👩đŸģ‍❤ī¸â€đŸ‘¨đŸŧ", + [.light, .medium]: "👩đŸģ‍❤ī¸â€đŸ‘¨đŸŊ", + [.light, .mediumDark]: "👩đŸģ‍❤ī¸â€đŸ‘¨đŸž", + [.light, .dark]: "👩đŸģ‍❤ī¸â€đŸ‘¨đŸŋ", + [.mediumLight]: "👩đŸŧ‍❤ī¸â€đŸ‘¨đŸŧ", + [.mediumLight, .light]: "👩đŸŧ‍❤ī¸â€đŸ‘¨đŸģ", + [.mediumLight, .medium]: "👩đŸŧ‍❤ī¸â€đŸ‘¨đŸŊ", + [.mediumLight, .mediumDark]: "👩đŸŧ‍❤ī¸â€đŸ‘¨đŸž", + [.mediumLight, .dark]: "👩đŸŧ‍❤ī¸â€đŸ‘¨đŸŋ", + [.medium]: "👩đŸŊ‍❤ī¸â€đŸ‘¨đŸŊ", + [.medium, .light]: "👩đŸŊ‍❤ī¸â€đŸ‘¨đŸģ", + [.medium, .mediumLight]: "👩đŸŊ‍❤ī¸â€đŸ‘¨đŸŧ", + [.medium, .mediumDark]: "👩đŸŊ‍❤ī¸â€đŸ‘¨đŸž", + [.medium, .dark]: "👩đŸŊ‍❤ī¸â€đŸ‘¨đŸŋ", + [.mediumDark]: "👩🏾‍❤ī¸â€đŸ‘¨đŸž", + [.mediumDark, .light]: "👩🏾‍❤ī¸â€đŸ‘¨đŸģ", + [.mediumDark, .mediumLight]: "👩🏾‍❤ī¸â€đŸ‘¨đŸŧ", + [.mediumDark, .medium]: "👩🏾‍❤ī¸â€đŸ‘¨đŸŊ", + [.mediumDark, .dark]: "👩🏾‍❤ī¸â€đŸ‘¨đŸŋ", + [.dark]: "👩đŸŋ‍❤ī¸â€đŸ‘¨đŸŋ", + [.dark, .light]: "👩đŸŋ‍❤ī¸â€đŸ‘¨đŸģ", + [.dark, .mediumLight]: "👩đŸŋ‍❤ī¸â€đŸ‘¨đŸŧ", + [.dark, .medium]: "👩đŸŋ‍❤ī¸â€đŸ‘¨đŸŊ", + [.dark, .mediumDark]: "👩đŸŋ‍❤ī¸â€đŸ‘¨đŸž", + ] + case .manHeartMan: + return [ + [.light]: "👨đŸģ‍❤ī¸â€đŸ‘¨đŸģ", + [.light, .mediumLight]: "👨đŸģ‍❤ī¸â€đŸ‘¨đŸŧ", + [.light, .medium]: "👨đŸģ‍❤ī¸â€đŸ‘¨đŸŊ", + [.light, .mediumDark]: "👨đŸģ‍❤ī¸â€đŸ‘¨đŸž", + [.light, .dark]: "👨đŸģ‍❤ī¸â€đŸ‘¨đŸŋ", + [.mediumLight]: "👨đŸŧ‍❤ī¸â€đŸ‘¨đŸŧ", + [.mediumLight, .light]: "👨đŸŧ‍❤ī¸â€đŸ‘¨đŸģ", + [.mediumLight, .medium]: "👨đŸŧ‍❤ī¸â€đŸ‘¨đŸŊ", + [.mediumLight, .mediumDark]: "👨đŸŧ‍❤ī¸â€đŸ‘¨đŸž", + [.mediumLight, .dark]: "👨đŸŧ‍❤ī¸â€đŸ‘¨đŸŋ", + [.medium]: "👨đŸŊ‍❤ī¸â€đŸ‘¨đŸŊ", + [.medium, .light]: "👨đŸŊ‍❤ī¸â€đŸ‘¨đŸģ", + [.medium, .mediumLight]: "👨đŸŊ‍❤ī¸â€đŸ‘¨đŸŧ", + [.medium, .mediumDark]: "👨đŸŊ‍❤ī¸â€đŸ‘¨đŸž", + [.medium, .dark]: "👨đŸŊ‍❤ī¸â€đŸ‘¨đŸŋ", + [.mediumDark]: "👨🏾‍❤ī¸â€đŸ‘¨đŸž", + [.mediumDark, .light]: "👨🏾‍❤ī¸â€đŸ‘¨đŸģ", + [.mediumDark, .mediumLight]: "👨🏾‍❤ī¸â€đŸ‘¨đŸŧ", + [.mediumDark, .medium]: "👨🏾‍❤ī¸â€đŸ‘¨đŸŊ", + [.mediumDark, .dark]: "👨🏾‍❤ī¸â€đŸ‘¨đŸŋ", + [.dark]: "👨đŸŋ‍❤ī¸â€đŸ‘¨đŸŋ", + [.dark, .light]: "👨đŸŋ‍❤ī¸â€đŸ‘¨đŸģ", + [.dark, .mediumLight]: "👨đŸŋ‍❤ī¸â€đŸ‘¨đŸŧ", + [.dark, .medium]: "👨đŸŋ‍❤ī¸â€đŸ‘¨đŸŊ", + [.dark, .mediumDark]: "👨đŸŋ‍❤ī¸â€đŸ‘¨đŸž", + ] + case .womanHeartWoman: + return [ + [.light]: "👩đŸģ‍❤ī¸â€đŸ‘ŠđŸģ", + [.light, .mediumLight]: "👩đŸģ‍❤ī¸â€đŸ‘ŠđŸŧ", + [.light, .medium]: "👩đŸģ‍❤ī¸â€đŸ‘ŠđŸŊ", + [.light, .mediumDark]: "👩đŸģ‍❤ī¸â€đŸ‘ŠđŸž", + [.light, .dark]: "👩đŸģ‍❤ī¸â€đŸ‘ŠđŸŋ", + [.mediumLight]: "👩đŸŧ‍❤ī¸â€đŸ‘ŠđŸŧ", + [.mediumLight, .light]: "👩đŸŧ‍❤ī¸â€đŸ‘ŠđŸģ", + [.mediumLight, .medium]: "👩đŸŧ‍❤ī¸â€đŸ‘ŠđŸŊ", + [.mediumLight, .mediumDark]: "👩đŸŧ‍❤ī¸â€đŸ‘ŠđŸž", + [.mediumLight, .dark]: "👩đŸŧ‍❤ī¸â€đŸ‘ŠđŸŋ", + [.medium]: "👩đŸŊ‍❤ī¸â€đŸ‘ŠđŸŊ", + [.medium, .light]: "👩đŸŊ‍❤ī¸â€đŸ‘ŠđŸģ", + [.medium, .mediumLight]: "👩đŸŊ‍❤ī¸â€đŸ‘ŠđŸŧ", + [.medium, .mediumDark]: "👩đŸŊ‍❤ī¸â€đŸ‘ŠđŸž", + [.medium, .dark]: "👩đŸŊ‍❤ī¸â€đŸ‘ŠđŸŋ", + [.mediumDark]: "👩🏾‍❤ī¸â€đŸ‘ŠđŸž", + [.mediumDark, .light]: "👩🏾‍❤ī¸â€đŸ‘ŠđŸģ", + [.mediumDark, .mediumLight]: "👩🏾‍❤ī¸â€đŸ‘ŠđŸŧ", + [.mediumDark, .medium]: "👩🏾‍❤ī¸â€đŸ‘ŠđŸŊ", + [.mediumDark, .dark]: "👩🏾‍❤ī¸â€đŸ‘ŠđŸŋ", + [.dark]: "👩đŸŋ‍❤ī¸â€đŸ‘ŠđŸŋ", + [.dark, .light]: "👩đŸŋ‍❤ī¸â€đŸ‘ŠđŸģ", + [.dark, .mediumLight]: "👩đŸŋ‍❤ī¸â€đŸ‘ŠđŸŧ", + [.dark, .medium]: "👩đŸŋ‍❤ī¸â€đŸ‘ŠđŸŊ", + [.dark, .mediumDark]: "👩đŸŋ‍❤ī¸â€đŸ‘ŠđŸž", + ] + default: return nil + } + } +} diff --git a/Session/Emoji/Emoji.swift b/Session/Emoji/Emoji.swift new file mode 100644 index 000000000..096dae434 --- /dev/null +++ b/Session/Emoji/Emoji.swift @@ -0,0 +1,1863 @@ + +// This file is generated by EmojiGenerator.swift, do not manually edit it. + +// swiftlint:disable all + +/// A sorted representation of all available emoji +enum Emoji: String, CaseIterable, Equatable { + case grinning = "😀" + case smiley = "😃" + case smile = "😄" + case grin = "😁" + case laughing = "😆" + case sweatSmile = "😅" + case rollingOnTheFloorLaughing = "đŸ¤Ŗ" + case joy = "😂" + case slightlySmilingFace = "🙂" + case upsideDownFace = "🙃" + case meltingFace = "đŸĢ " + case wink = "😉" + case blush = "😊" + case innocent = "😇" + case smilingFaceWith3Hearts = "đŸĨ°" + case heartEyes = "😍" + case starStruck = "🤩" + case kissingHeart = "😘" + case kissing = "😗" + case relaxed = "â˜ēī¸" + case kissingClosedEyes = "😚" + case kissingSmilingEyes = "😙" + case smilingFaceWithTear = "đŸĨ˛" + case yum = "😋" + case stuckOutTongue = "😛" + case stuckOutTongueWinkingEye = "😜" + case zanyFace = "đŸ¤Ē" + case stuckOutTongueClosedEyes = "😝" + case moneyMouthFace = "🤑" + case huggingFace = "🤗" + case faceWithHandOverMouth = "🤭" + case faceWithOpenEyesAndHandOverMouth = "đŸĢĸ" + case faceWithPeekingEye = "đŸĢŖ" + case shushingFace = "đŸ¤Ģ" + case thinkingFace = "🤔" + case salutingFace = "đŸĢĄ" + case zipperMouthFace = "🤐" + case faceWithRaisedEyebrow = "🤨" + case neutralFace = "😐" + case expressionless = "😑" + case noMouth = "đŸ˜ļ" + case dottedLineFace = "đŸĢĨ" + case faceInClouds = "đŸ˜ļ‍đŸŒĢī¸" + case smirk = "😏" + case unamused = "😒" + case faceWithRollingEyes = "🙄" + case grimacing = "đŸ˜Ŧ" + case faceExhaling = "😮‍💨" + case lyingFace = "đŸ¤Ĩ" + case relieved = "😌" + case pensive = "😔" + case sleepy = "đŸ˜Ē" + case droolingFace = "🤤" + case sleeping = "😴" + case mask = "😷" + case faceWithThermometer = "🤒" + case faceWithHeadBandage = "🤕" + case nauseatedFace = "đŸ¤ĸ" + case faceVomiting = "🤮" + case sneezingFace = "🤧" + case hotFace = "đŸĨĩ" + case coldFace = "đŸĨļ" + case woozyFace = "đŸĨ´" + case dizzyFace = "đŸ˜ĩ" + case faceWithSpiralEyes = "đŸ˜ĩ‍đŸ’Ģ" + case explodingHead = "đŸ¤¯" + case faceWithCowboyHat = "🤠" + case partyingFace = "đŸĨŗ" + case disguisedFace = "đŸĨ¸" + case sunglasses = "😎" + case nerdFace = "🤓" + case faceWithMonocle = "🧐" + case confused = "😕" + case faceWithDiagonalMouth = "đŸĢ¤" + case worried = "😟" + case slightlyFrowningFace = "🙁" + case whiteFrowningFace = "☚ī¸" + case openMouth = "😮" + case hushed = "đŸ˜¯" + case astonished = "😲" + case flushed = "đŸ˜ŗ" + case pleadingFace = "đŸĨē" + case faceHoldingBackTears = "đŸĨš" + case frowning = "đŸ˜Ļ" + case anguished = "😧" + case fearful = "😨" + case coldSweat = "😰" + case disappointedRelieved = "đŸ˜Ĩ" + case cry = "đŸ˜ĸ" + case sob = "😭" + case scream = "😱" + case confounded = "😖" + case persevere = "đŸ˜Ŗ" + case disappointed = "😞" + case sweat = "😓" + case weary = "😩" + case tiredFace = "đŸ˜Ģ" + case yawningFace = "đŸĨą" + case triumph = "😤" + case rage = "😡" + case angry = "😠" + case faceWithSymbolsOnMouth = "đŸ¤Ŧ" + case smilingImp = "😈" + case imp = "đŸ‘ŋ" + case skull = "💀" + case skullAndCrossbones = "☠ī¸" + case hankey = "💩" + case clownFace = "🤡" + case japaneseOgre = "👹" + case japaneseGoblin = "đŸ‘ē" + case ghost = "đŸ‘ģ" + case alien = "đŸ‘Ŋ" + case spaceInvader = "👾" + case robotFace = "🤖" + case smileyCat = "đŸ˜ē" + case smileCat = "😸" + case joyCat = "😹" + case heartEyesCat = "đŸ˜ģ" + case smirkCat = "đŸ˜ŧ" + case kissingCat = "đŸ˜Ŋ" + case screamCat = "🙀" + case cryingCatFace = "đŸ˜ŋ" + case poutingCat = "😾" + case seeNoEvil = "🙈" + case hearNoEvil = "🙉" + case speakNoEvil = "🙊" + case kiss = "💋" + case loveLetter = "💌" + case cupid = "💘" + case giftHeart = "💝" + case sparklingHeart = "💖" + case heartpulse = "💗" + case heartbeat = "💓" + case revolvingHearts = "💞" + case twoHearts = "💕" + case heartDecoration = "💟" + case heavyHeartExclamationMarkOrnament = "âŖī¸" + case brokenHeart = "💔" + case heartOnFire = "❤ī¸â€đŸ”Ĩ" + case mendingHeart = "❤ī¸â€đŸŠš" + case heart = "❤ī¸" + case orangeHeart = "🧡" + case yellowHeart = "💛" + case greenHeart = "💚" + case blueHeart = "💙" + case purpleHeart = "💜" + case brownHeart = "🤎" + case blackHeart = "🖤" + case whiteHeart = "🤍" + case oneHundred = "đŸ’¯" + case anger = "đŸ’ĸ" + case boom = "đŸ’Ĩ" + case dizzy = "đŸ’Ģ" + case sweatDrops = "đŸ’Ļ" + case dash = "💨" + case hole = "đŸ•ŗī¸" + case bomb = "đŸ’Ŗ" + case speechBalloon = "đŸ’Ŧ" + case eyeInSpeechBubble = "👁ī¸â€đŸ—¨ī¸" + case leftSpeechBubble = "🗨ī¸" + case rightAngerBubble = "đŸ—¯ī¸" + case thoughtBalloon = "💭" + case zzz = "💤" + case wave = "👋" + case raisedBackOfHand = "🤚" + case raisedHandWithFingersSplayed = "🖐ī¸" + case hand = "✋" + case spockHand = "🖖" + case rightwardsHand = "đŸĢą" + case leftwardsHand = "đŸĢ˛" + case palmDownHand = "đŸĢŗ" + case palmUpHand = "đŸĢ´" + case okHand = "👌" + case pinchedFingers = "🤌" + case pinchingHand = "🤏" + case v = "✌ī¸" + case crossedFingers = "🤞" + case handWithIndexFingerAndThumbCrossed = "đŸĢ°" + case iLoveYouHandSign = "🤟" + case theHorns = "🤘" + case callMeHand = "🤙" + case pointLeft = "👈" + case pointRight = "👉" + case pointUp2 = "👆" + case middleFinger = "🖕" + case pointDown = "👇" + case pointUp = "☝ī¸" + case indexPointingAtTheViewer = "đŸĢĩ" + case plusOne = "👍" + case negativeOne = "👎" + case fist = "✊" + case facepunch = "👊" + case leftFacingFist = "🤛" + case rightFacingFist = "🤜" + case clap = "👏" + case raisedHands = "🙌" + case heartHands = "đŸĢļ" + case openHands = "👐" + case palmsUpTogether = "🤲" + case handshake = "🤝" + case pray = "🙏" + case writingHand = "✍ī¸" + case nailCare = "💅" + case selfie = "đŸ¤ŗ" + case muscle = "đŸ’Ē" + case mechanicalArm = "đŸĻž" + case mechanicalLeg = "đŸĻŋ" + case leg = "đŸĻĩ" + case foot = "đŸĻļ" + case ear = "👂" + case earWithHearingAid = "đŸĻģ" + case nose = "👃" + case brain = "🧠" + case anatomicalHeart = "đŸĢ€" + case lungs = "đŸĢ" + case tooth = "đŸĻˇ" + case bone = "đŸĻ´" + case eyes = "👀" + case eye = "👁ī¸" + case tongue = "👅" + case lips = "👄" + case bitingLip = "đŸĢĻ" + case baby = "đŸ‘ļ" + case child = "🧒" + case boy = "đŸ‘Ļ" + case girl = "👧" + case adult = "🧑" + case personWithBlondHair = "👱" + case man = "👨" + case beardedPerson = "🧔" + case manWithBeard = "🧔‍♂ī¸" + case womanWithBeard = "🧔‍♀ī¸" + case redHairedMan = "👨‍đŸĻ°" + case curlyHairedMan = "👨‍đŸĻą" + case whiteHairedMan = "👨‍đŸĻŗ" + case baldMan = "👨‍đŸĻ˛" + case woman = "👩" + case redHairedWoman = "👩‍đŸĻ°" + case redHairedPerson = "🧑‍đŸĻ°" + case curlyHairedWoman = "👩‍đŸĻą" + case curlyHairedPerson = "🧑‍đŸĻą" + case whiteHairedWoman = "👩‍đŸĻŗ" + case whiteHairedPerson = "🧑‍đŸĻŗ" + case baldWoman = "👩‍đŸĻ˛" + case baldPerson = "🧑‍đŸĻ˛" + case blondHairedWoman = "👱‍♀ī¸" + case blondHairedMan = "👱‍♂ī¸" + case olderAdult = "🧓" + case olderMan = "👴" + case olderWoman = "đŸ‘ĩ" + case personFrowning = "🙍" + case manFrowning = "🙍‍♂ī¸" + case womanFrowning = "🙍‍♀ī¸" + case personWithPoutingFace = "🙎" + case manPouting = "🙎‍♂ī¸" + case womanPouting = "🙎‍♀ī¸" + case noGood = "🙅" + case manGesturingNo = "🙅‍♂ī¸" + case womanGesturingNo = "🙅‍♀ī¸" + case okWoman = "🙆" + case manGesturingOk = "🙆‍♂ī¸" + case womanGesturingOk = "🙆‍♀ī¸" + case informationDeskPerson = "💁" + case manTippingHand = "💁‍♂ī¸" + case womanTippingHand = "💁‍♀ī¸" + case raisingHand = "🙋" + case manRaisingHand = "🙋‍♂ī¸" + case womanRaisingHand = "🙋‍♀ī¸" + case deafPerson = "🧏" + case deafMan = "🧏‍♂ī¸" + case deafWoman = "🧏‍♀ī¸" + case bow = "🙇" + case manBowing = "🙇‍♂ī¸" + case womanBowing = "🙇‍♀ī¸" + case facePalm = "đŸ¤Ļ" + case manFacepalming = "đŸ¤Ļ‍♂ī¸" + case womanFacepalming = "đŸ¤Ļ‍♀ī¸" + case shrug = "🤷" + case manShrugging = "🤷‍♂ī¸" + case womanShrugging = "🤷‍♀ī¸" + case healthWorker = "🧑‍⚕ī¸" + case maleDoctor = "👨‍⚕ī¸" + case femaleDoctor = "👩‍⚕ī¸" + case student = "🧑‍🎓" + case maleStudent = "👨‍🎓" + case femaleStudent = "👩‍🎓" + case teacher = "🧑‍đŸĢ" + case maleTeacher = "👨‍đŸĢ" + case femaleTeacher = "👩‍đŸĢ" + case judge = "🧑‍⚖ī¸" + case maleJudge = "👨‍⚖ī¸" + case femaleJudge = "👩‍⚖ī¸" + case farmer = "🧑‍🌾" + case maleFarmer = "👨‍🌾" + case femaleFarmer = "👩‍🌾" + case cook = "🧑‍đŸŗ" + case maleCook = "👨‍đŸŗ" + case femaleCook = "👩‍đŸŗ" + case mechanic = "🧑‍🔧" + case maleMechanic = "👨‍🔧" + case femaleMechanic = "👩‍🔧" + case factoryWorker = "🧑‍🏭" + case maleFactoryWorker = "👨‍🏭" + case femaleFactoryWorker = "👩‍🏭" + case officeWorker = "🧑‍đŸ’ŧ" + case maleOfficeWorker = "👨‍đŸ’ŧ" + case femaleOfficeWorker = "👩‍đŸ’ŧ" + case scientist = "🧑‍đŸ”Ŧ" + case maleScientist = "👨‍đŸ”Ŧ" + case femaleScientist = "👩‍đŸ”Ŧ" + case technologist = "🧑‍đŸ’ģ" + case maleTechnologist = "👨‍đŸ’ģ" + case femaleTechnologist = "👩‍đŸ’ģ" + case singer = "🧑‍🎤" + case maleSinger = "👨‍🎤" + case femaleSinger = "👩‍🎤" + case artist = "🧑‍🎨" + case maleArtist = "👨‍🎨" + case femaleArtist = "👩‍🎨" + case pilot = "🧑‍✈ī¸" + case malePilot = "👨‍✈ī¸" + case femalePilot = "👩‍✈ī¸" + case astronaut = "🧑‍🚀" + case maleAstronaut = "👨‍🚀" + case femaleAstronaut = "👩‍🚀" + case firefighter = "🧑‍🚒" + case maleFirefighter = "👨‍🚒" + case femaleFirefighter = "👩‍🚒" + case cop = "👮" + case malePoliceOfficer = "👮‍♂ī¸" + case femalePoliceOfficer = "👮‍♀ī¸" + case sleuthOrSpy = "đŸ•ĩī¸" + case maleDetective = "đŸ•ĩī¸â€â™‚ī¸" + case femaleDetective = "đŸ•ĩī¸â€â™€ī¸" + case guardsman = "💂" + case maleGuard = "💂‍♂ī¸" + case femaleGuard = "💂‍♀ī¸" + case ninja = "đŸĨˇ" + case constructionWorker = "👷" + case maleConstructionWorker = "👷‍♂ī¸" + case femaleConstructionWorker = "👷‍♀ī¸" + case personWithCrown = "đŸĢ…" + case prince = "🤴" + case princess = "👸" + case manWithTurban = "đŸ‘ŗ" + case manWearingTurban = "đŸ‘ŗ‍♂ī¸" + case womanWearingTurban = "đŸ‘ŗ‍♀ī¸" + case manWithGuaPiMao = "👲" + case personWithHeadscarf = "🧕" + case personInTuxedo = "đŸ¤ĩ" + case manInTuxedo = "đŸ¤ĩ‍♂ī¸" + case womanInTuxedo = "đŸ¤ĩ‍♀ī¸" + case brideWithVeil = "👰" + case manWithVeil = "👰‍♂ī¸" + case womanWithVeil = "👰‍♀ī¸" + case pregnantWoman = "🤰" + case pregnantMan = "đŸĢƒ" + case pregnantPerson = "đŸĢ„" + case breastFeeding = "🤱" + case womanFeedingBaby = "👩‍đŸŧ" + case manFeedingBaby = "👨‍đŸŧ" + case personFeedingBaby = "🧑‍đŸŧ" + case angel = "đŸ‘ŧ" + case santa = "🎅" + case mrsClaus = "đŸ¤ļ" + case mxClaus = "🧑‍🎄" + case superhero = "đŸĻ¸" + case maleSuperhero = "đŸĻ¸â€â™‚ī¸" + case femaleSuperhero = "đŸĻ¸â€â™€ī¸" + case supervillain = "đŸĻš" + case maleSupervillain = "đŸĻšâ€â™‚ī¸" + case femaleSupervillain = "đŸĻšâ€â™€ī¸" + case mage = "🧙" + case maleMage = "🧙‍♂ī¸" + case femaleMage = "🧙‍♀ī¸" + case fairy = "🧚" + case maleFairy = "🧚‍♂ī¸" + case femaleFairy = "🧚‍♀ī¸" + case vampire = "🧛" + case maleVampire = "🧛‍♂ī¸" + case femaleVampire = "🧛‍♀ī¸" + case merperson = "🧜" + case merman = "🧜‍♂ī¸" + case mermaid = "🧜‍♀ī¸" + case elf = "🧝" + case maleElf = "🧝‍♂ī¸" + case femaleElf = "🧝‍♀ī¸" + case genie = "🧞" + case maleGenie = "🧞‍♂ī¸" + case femaleGenie = "🧞‍♀ī¸" + case zombie = "🧟" + case maleZombie = "🧟‍♂ī¸" + case femaleZombie = "🧟‍♀ī¸" + case troll = "🧌" + case massage = "💆" + case manGettingMassage = "💆‍♂ī¸" + case womanGettingMassage = "💆‍♀ī¸" + case haircut = "💇" + case manGettingHaircut = "💇‍♂ī¸" + case womanGettingHaircut = "💇‍♀ī¸" + case walking = "đŸšļ" + case manWalking = "đŸšļ‍♂ī¸" + case womanWalking = "đŸšļ‍♀ī¸" + case standingPerson = "🧍" + case manStanding = "🧍‍♂ī¸" + case womanStanding = "🧍‍♀ī¸" + case kneelingPerson = "🧎" + case manKneeling = "🧎‍♂ī¸" + case womanKneeling = "🧎‍♀ī¸" + case personWithProbingCane = "🧑‍đŸĻ¯" + case manWithProbingCane = "👨‍đŸĻ¯" + case womanWithProbingCane = "👩‍đŸĻ¯" + case personInMotorizedWheelchair = "🧑‍đŸĻŧ" + case manInMotorizedWheelchair = "👨‍đŸĻŧ" + case womanInMotorizedWheelchair = "👩‍đŸĻŧ" + case personInManualWheelchair = "🧑‍đŸĻŊ" + case manInManualWheelchair = "👨‍đŸĻŊ" + case womanInManualWheelchair = "👩‍đŸĻŊ" + case runner = "🏃" + case manRunning = "🏃‍♂ī¸" + case womanRunning = "🏃‍♀ī¸" + case dancer = "💃" + case manDancing = "đŸ•ē" + case manInBusinessSuitLevitating = "🕴ī¸" + case dancers = "đŸ‘¯" + case menWithBunnyEarsPartying = "đŸ‘¯â€â™‚ī¸" + case womenWithBunnyEarsPartying = "đŸ‘¯â€â™€ī¸" + case personInSteamyRoom = "🧖" + case manInSteamyRoom = "🧖‍♂ī¸" + case womanInSteamyRoom = "🧖‍♀ī¸" + case personClimbing = "🧗" + case manClimbing = "🧗‍♂ī¸" + case womanClimbing = "🧗‍♀ī¸" + case fencer = "đŸ¤ē" + case horseRacing = "🏇" + case skier = "⛷ī¸" + case snowboarder = "🏂" + case golfer = "🏌ī¸" + case manGolfing = "🏌ī¸â€â™‚ī¸" + case womanGolfing = "🏌ī¸â€â™€ī¸" + case surfer = "🏄" + case manSurfing = "🏄‍♂ī¸" + case womanSurfing = "🏄‍♀ī¸" + case rowboat = "đŸšŖ" + case manRowingBoat = "đŸšŖ‍♂ī¸" + case womanRowingBoat = "đŸšŖ‍♀ī¸" + case swimmer = "🏊" + case manSwimming = "🏊‍♂ī¸" + case womanSwimming = "🏊‍♀ī¸" + case personWithBall = "⛹ī¸" + case manBouncingBall = "⛹ī¸â€â™‚ī¸" + case womanBouncingBall = "⛹ī¸â€â™€ī¸" + case weightLifter = "🏋ī¸" + case manLiftingWeights = "🏋ī¸â€â™‚ī¸" + case womanLiftingWeights = "🏋ī¸â€â™€ī¸" + case bicyclist = "🚴" + case manBiking = "🚴‍♂ī¸" + case womanBiking = "🚴‍♀ī¸" + case mountainBicyclist = "đŸšĩ" + case manMountainBiking = "đŸšĩ‍♂ī¸" + case womanMountainBiking = "đŸšĩ‍♀ī¸" + case personDoingCartwheel = "🤸" + case manCartwheeling = "🤸‍♂ī¸" + case womanCartwheeling = "🤸‍♀ī¸" + case wrestlers = "đŸ¤ŧ" + case manWrestling = "đŸ¤ŧ‍♂ī¸" + case womanWrestling = "đŸ¤ŧ‍♀ī¸" + case waterPolo = "đŸ¤Ŋ" + case manPlayingWaterPolo = "đŸ¤Ŋ‍♂ī¸" + case womanPlayingWaterPolo = "đŸ¤Ŋ‍♀ī¸" + case handball = "🤾" + case manPlayingHandball = "🤾‍♂ī¸" + case womanPlayingHandball = "🤾‍♀ī¸" + case juggling = "🤹" + case manJuggling = "🤹‍♂ī¸" + case womanJuggling = "🤹‍♀ī¸" + case personInLotusPosition = "🧘" + case manInLotusPosition = "🧘‍♂ī¸" + case womanInLotusPosition = "🧘‍♀ī¸" + case bath = "🛀" + case sleepingAccommodation = "🛌" + case peopleHoldingHands = "🧑‍🤝‍🧑" + case twoWomenHoldingHands = "👭" + case manAndWomanHoldingHands = "đŸ‘Ģ" + case twoMenHoldingHands = "đŸ‘Ŧ" + case personKissPerson = "💏" + case womanKissMan = "👩‍❤ī¸â€đŸ’‹â€đŸ‘¨" + case manKissMan = "👨‍❤ī¸â€đŸ’‹â€đŸ‘¨" + case womanKissWoman = "👩‍❤ī¸â€đŸ’‹â€đŸ‘Š" + case personHeartPerson = "💑" + case womanHeartMan = "👩‍❤ī¸â€đŸ‘¨" + case manHeartMan = "👨‍❤ī¸â€đŸ‘¨" + case womanHeartWoman = "👩‍❤ī¸â€đŸ‘Š" + case family = "đŸ‘Ē" + case manWomanBoy = "👨‍👩‍đŸ‘Ļ" + case manWomanGirl = "👨‍👩‍👧" + case manWomanGirlBoy = "👨‍👩‍👧‍đŸ‘Ļ" + case manWomanBoyBoy = "👨‍👩‍đŸ‘Ļ‍đŸ‘Ļ" + case manWomanGirlGirl = "👨‍👩‍👧‍👧" + case manManBoy = "👨‍👨‍đŸ‘Ļ" + case manManGirl = "👨‍👨‍👧" + case manManGirlBoy = "👨‍👨‍👧‍đŸ‘Ļ" + case manManBoyBoy = "👨‍👨‍đŸ‘Ļ‍đŸ‘Ļ" + case manManGirlGirl = "👨‍👨‍👧‍👧" + case womanWomanBoy = "👩‍👩‍đŸ‘Ļ" + case womanWomanGirl = "👩‍👩‍👧" + case womanWomanGirlBoy = "👩‍👩‍👧‍đŸ‘Ļ" + case womanWomanBoyBoy = "👩‍👩‍đŸ‘Ļ‍đŸ‘Ļ" + case womanWomanGirlGirl = "👩‍👩‍👧‍👧" + case manBoy = "👨‍đŸ‘Ļ" + case manBoyBoy = "👨‍đŸ‘Ļ‍đŸ‘Ļ" + case manGirl = "👨‍👧" + case manGirlBoy = "👨‍👧‍đŸ‘Ļ" + case manGirlGirl = "👨‍👧‍👧" + case womanBoy = "👩‍đŸ‘Ļ" + case womanBoyBoy = "👩‍đŸ‘Ļ‍đŸ‘Ļ" + case womanGirl = "👩‍👧" + case womanGirlBoy = "👩‍👧‍đŸ‘Ļ" + case womanGirlGirl = "👩‍👧‍👧" + case speakingHeadInSilhouette = "đŸ—Ŗī¸" + case bustInSilhouette = "👤" + case bustsInSilhouette = "đŸ‘Ĩ" + case peopleHugging = "đŸĢ‚" + case footprints = "đŸ‘Ŗ" + case skinTone2 = "đŸģ" + case skinTone3 = "đŸŧ" + case skinTone4 = "đŸŊ" + case skinTone5 = "🏾" + case skinTone6 = "đŸŋ" + case monkeyFace = "đŸĩ" + case monkey = "🐒" + case gorilla = "đŸĻ" + case orangutan = "đŸĻ§" + case dog = "đŸļ" + case dog2 = "🐕" + case guideDog = "đŸĻŽ" + case serviceDog = "🐕‍đŸĻē" + case poodle = "🐩" + case wolf = "đŸē" + case foxFace = "đŸĻŠ" + case raccoon = "đŸĻ" + case cat = "🐱" + case cat2 = "🐈" + case blackCat = "🐈‍âŦ›" + case lionFace = "đŸĻ" + case tiger = "đŸ¯" + case tiger2 = "🐅" + case leopard = "🐆" + case horse = "🐴" + case racehorse = "🐎" + case unicornFace = "đŸĻ„" + case zebraFace = "đŸĻ“" + case deer = "đŸĻŒ" + case bison = "đŸĻŦ" + case cow = "🐮" + case ox = "🐂" + case waterBuffalo = "🐃" + case cow2 = "🐄" + case pig = "🐷" + case pig2 = "🐖" + case boar = "🐗" + case pigNose = "đŸŊ" + case ram = "🐏" + case sheep = "🐑" + case goat = "🐐" + case dromedaryCamel = "đŸĒ" + case camel = "đŸĢ" + case llama = "đŸĻ™" + case giraffeFace = "đŸĻ’" + case elephant = "🐘" + case mammoth = "đŸĻŖ" + case rhinoceros = "đŸĻ" + case hippopotamus = "đŸĻ›" + case mouse = "🐭" + case mouse2 = "🐁" + case rat = "🐀" + case hamster = "🐹" + case rabbit = "🐰" + case rabbit2 = "🐇" + case chipmunk = "đŸŋī¸" + case beaver = "đŸĻĢ" + case hedgehog = "đŸĻ”" + case bat = "đŸĻ‡" + case bear = "đŸģ" + case polarBear = "đŸģ‍❄ī¸" + case koala = "🐨" + case pandaFace = "đŸŧ" + case sloth = "đŸĻĨ" + case otter = "đŸĻĻ" + case skunk = "đŸĻ¨" + case kangaroo = "đŸĻ˜" + case badger = "đŸĻĄ" + case feet = "🐾" + case turkey = "đŸĻƒ" + case chicken = "🐔" + case rooster = "🐓" + case hatchingChick = "đŸŖ" + case babyChick = "🐤" + case hatchedChick = "đŸĨ" + case bird = "đŸĻ" + case penguin = "🐧" + case doveOfPeace = "🕊ī¸" + case eagle = "đŸĻ…" + case duck = "đŸĻ†" + case swan = "đŸĻĸ" + case owl = "đŸĻ‰" + case dodo = "đŸĻ¤" + case feather = "đŸĒļ" + case flamingo = "đŸĻŠ" + case peacock = "đŸĻš" + case parrot = "đŸĻœ" + case frog = "🐸" + case crocodile = "🐊" + case turtle = "đŸĸ" + case lizard = "đŸĻŽ" + case snake = "🐍" + case dragonFace = "🐲" + case dragon = "🐉" + case sauropod = "đŸĻ•" + case tRex = "đŸĻ–" + case whale = "đŸŗ" + case whale2 = "🐋" + case dolphin = "đŸŦ" + case seal = "đŸĻ­" + case fish = "🐟" + case tropicalFish = "🐠" + case blowfish = "🐡" + case shark = "đŸĻˆ" + case octopus = "🐙" + case shell = "🐚" + case coral = "đŸĒ¸" + case snail = "🐌" + case butterfly = "đŸĻ‹" + case bug = "🐛" + case ant = "🐜" + case bee = "🐝" + case beetle = "đŸĒ˛" + case ladybug = "🐞" + case cricket = "đŸĻ—" + case cockroach = "đŸĒŗ" + case spider = "🕷ī¸" + case spiderWeb = "🕸ī¸" + case scorpion = "đŸĻ‚" + case mosquito = "đŸĻŸ" + case fly = "đŸĒ°" + case worm = "đŸĒą" + case microbe = "đŸĻ " + case bouquet = "💐" + case cherryBlossom = "🌸" + case whiteFlower = "💮" + case lotus = "đŸĒˇ" + case rosette = "đŸĩī¸" + case rose = "🌹" + case wiltedFlower = "đŸĨ€" + case hibiscus = "đŸŒē" + case sunflower = "đŸŒģ" + case blossom = "đŸŒŧ" + case tulip = "🌷" + case seedling = "🌱" + case pottedPlant = "đŸĒ´" + case evergreenTree = "🌲" + case deciduousTree = "đŸŒŗ" + case palmTree = "🌴" + case cactus = "đŸŒĩ" + case earOfRice = "🌾" + case herb = "đŸŒŋ" + case shamrock = "☘ī¸" + case fourLeafClover = "🍀" + case mapleLeaf = "🍁" + case fallenLeaf = "🍂" + case leaves = "🍃" + case emptyNest = "đŸĒš" + case nestWithEggs = "đŸĒē" + case grapes = "🍇" + case melon = "🍈" + case watermelon = "🍉" + case tangerine = "🍊" + case lemon = "🍋" + case banana = "🍌" + case pineapple = "🍍" + case mango = "đŸĨ­" + case apple = "🍎" + case greenApple = "🍏" + case pear = "🍐" + case peach = "🍑" + case cherries = "🍒" + case strawberry = "🍓" + case blueberries = "đŸĢ" + case kiwifruit = "đŸĨ" + case tomato = "🍅" + case olive = "đŸĢ’" + case coconut = "đŸĨĨ" + case avocado = "đŸĨ‘" + case eggplant = "🍆" + case potato = "đŸĨ”" + case carrot = "đŸĨ•" + case corn = "đŸŒŊ" + case hotPepper = "đŸŒļī¸" + case bellPepper = "đŸĢ‘" + case cucumber = "đŸĨ’" + case leafyGreen = "đŸĨŦ" + case broccoli = "đŸĨĻ" + case garlic = "🧄" + case onion = "🧅" + case mushroom = "🍄" + case peanuts = "đŸĨœ" + case beans = "đŸĢ˜" + case chestnut = "🌰" + case bread = "🍞" + case croissant = "đŸĨ" + case baguetteBread = "đŸĨ–" + case flatbread = "đŸĢ“" + case pretzel = "đŸĨ¨" + case bagel = "đŸĨ¯" + case pancakes = "đŸĨž" + case waffle = "🧇" + case cheeseWedge = "🧀" + case meatOnBone = "🍖" + case poultryLeg = "🍗" + case cutOfMeat = "đŸĨŠ" + case bacon = "đŸĨ“" + case hamburger = "🍔" + case fries = "🍟" + case pizza = "🍕" + case hotdog = "🌭" + case sandwich = "đŸĨĒ" + case taco = "🌮" + case burrito = "đŸŒ¯" + case tamale = "đŸĢ”" + case stuffedFlatbread = "đŸĨ™" + case falafel = "🧆" + case egg = "đŸĨš" + case friedEgg = "đŸŗ" + case shallowPanOfFood = "đŸĨ˜" + case stew = "🍲" + case fondue = "đŸĢ•" + case bowlWithSpoon = "đŸĨŖ" + case greenSalad = "đŸĨ—" + case popcorn = "đŸŋ" + case butter = "🧈" + case salt = "🧂" + case cannedFood = "đŸĨĢ" + case bento = "🍱" + case riceCracker = "🍘" + case riceBall = "🍙" + case rice = "🍚" + case curry = "🍛" + case ramen = "🍜" + case spaghetti = "🍝" + case sweetPotato = "🍠" + case oden = "đŸĸ" + case sushi = "đŸŖ" + case friedShrimp = "🍤" + case fishCake = "đŸĨ" + case moonCake = "đŸĨŽ" + case dango = "🍡" + case dumpling = "đŸĨŸ" + case fortuneCookie = "đŸĨ " + case takeoutBox = "đŸĨĄ" + case crab = "đŸĻ€" + case lobster = "đŸĻž" + case shrimp = "đŸĻ" + case squid = "đŸĻ‘" + case oyster = "đŸĻĒ" + case icecream = "đŸĻ" + case shavedIce = "🍧" + case iceCream = "🍨" + case doughnut = "🍩" + case cookie = "đŸĒ" + case birthday = "🎂" + case cake = "🍰" + case cupcake = "🧁" + case pie = "đŸĨ§" + case chocolateBar = "đŸĢ" + case candy = "đŸŦ" + case lollipop = "🍭" + case custard = "🍮" + case honeyPot = "đŸ¯" + case babyBottle = "đŸŧ" + case glassOfMilk = "đŸĨ›" + case coffee = "☕" + case teapot = "đŸĢ–" + case tea = "đŸĩ" + case sake = "đŸļ" + case champagne = "🍾" + case wineGlass = "🍷" + case cocktail = "🍸" + case tropicalDrink = "🍹" + case beer = "đŸē" + case beers = "đŸģ" + case clinkingGlasses = "đŸĨ‚" + case tumblerGlass = "đŸĨƒ" + case pouringLiquid = "đŸĢ—" + case cupWithStraw = "đŸĨ¤" + case bubbleTea = "🧋" + case beverageBox = "🧃" + case mateDrink = "🧉" + case iceCube = "🧊" + case chopsticks = "đŸĨĸ" + case knifeForkPlate = "đŸŊī¸" + case forkAndKnife = "🍴" + case spoon = "đŸĨ„" + case hocho = "đŸ”Ē" + case jar = "đŸĢ™" + case amphora = "đŸē" + case earthAfrica = "🌍" + case earthAmericas = "🌎" + case earthAsia = "🌏" + case globeWithMeridians = "🌐" + case worldMap = "đŸ—ēī¸" + case japan = "🗾" + case compass = "🧭" + case snowCappedMountain = "🏔ī¸" + case mountain = "⛰ī¸" + case volcano = "🌋" + case mountFuji = "đŸ—ģ" + case camping = "🏕ī¸" + case beachWithUmbrella = "🏖ī¸" + case desert = "🏜ī¸" + case desertIsland = "🏝ī¸" + case nationalPark = "🏞ī¸" + case stadium = "🏟ī¸" + case classicalBuilding = "🏛ī¸" + case buildingConstruction = "🏗ī¸" + case bricks = "🧱" + case rock = "đŸĒ¨" + case wood = "đŸĒĩ" + case hut = "🛖" + case houseBuildings = "🏘ī¸" + case derelictHouseBuilding = "🏚ī¸" + case house = "🏠" + case houseWithGarden = "🏡" + case office = "đŸĸ" + case postOffice = "đŸŖ" + case europeanPostOffice = "🏤" + case hospital = "đŸĨ" + case bank = "đŸĻ" + case hotel = "🏨" + case loveHotel = "🏩" + case convenienceStore = "đŸĒ" + case school = "đŸĢ" + case departmentStore = "đŸŦ" + case factory = "🏭" + case japaneseCastle = "đŸ¯" + case europeanCastle = "🏰" + case wedding = "💒" + case tokyoTower = "đŸ—ŧ" + case statueOfLiberty = "đŸ—Ŋ" + case church = "â›Ē" + case mosque = "🕌" + case hinduTemple = "🛕" + case synagogue = "🕍" + case shintoShrine = "⛩ī¸" + case kaaba = "🕋" + case fountain = "⛲" + case tent = "â›ē" + case foggy = "🌁" + case nightWithStars = "🌃" + case cityscape = "🏙ī¸" + case sunriseOverMountains = "🌄" + case sunrise = "🌅" + case citySunset = "🌆" + case citySunrise = "🌇" + case bridgeAtNight = "🌉" + case hotsprings = "♨ī¸" + case carouselHorse = "🎠" + case playgroundSlide = "🛝" + case ferrisWheel = "🎡" + case rollerCoaster = "đŸŽĸ" + case barber = "💈" + case circusTent = "đŸŽĒ" + case steamLocomotive = "🚂" + case railwayCar = "🚃" + case bullettrainSide = "🚄" + case bullettrainFront = "🚅" + case train2 = "🚆" + case metro = "🚇" + case lightRail = "🚈" + case station = "🚉" + case tram = "🚊" + case monorail = "🚝" + case mountainRailway = "🚞" + case train = "🚋" + case bus = "🚌" + case oncomingBus = "🚍" + case trolleybus = "🚎" + case minibus = "🚐" + case ambulance = "🚑" + case fireEngine = "🚒" + case policeCar = "🚓" + case oncomingPoliceCar = "🚔" + case taxi = "🚕" + case oncomingTaxi = "🚖" + case car = "🚗" + case oncomingAutomobile = "🚘" + case blueCar = "🚙" + case pickupTruck = "đŸ›ģ" + case truck = "🚚" + case articulatedLorry = "🚛" + case tractor = "🚜" + case racingCar = "🏎ī¸" + case racingMotorcycle = "🏍ī¸" + case motorScooter = "đŸ›ĩ" + case manualWheelchair = "đŸĻŊ" + case motorizedWheelchair = "đŸĻŧ" + case autoRickshaw = "đŸ›ē" + case bike = "🚲" + case scooter = "🛴" + case skateboard = "🛹" + case rollerSkate = "đŸ›ŧ" + case busstop = "🚏" + case motorway = "đŸ›Ŗī¸" + case railwayTrack = "🛤ī¸" + case oilDrum = "đŸ›ĸī¸" + case fuelpump = "â›Ŋ" + case wheel = "🛞" + case rotatingLight = "🚨" + case trafficLight = "đŸšĨ" + case verticalTrafficLight = "đŸšĻ" + case octagonalSign = "🛑" + case construction = "🚧" + case anchor = "⚓" + case ringBuoy = "🛟" + case boat = "â›ĩ" + case canoe = "đŸ›ļ" + case speedboat = "🚤" + case passengerShip = "đŸ›ŗī¸" + case ferry = "⛴ī¸" + case motorBoat = "đŸ›Ĩī¸" + case ship = "đŸšĸ" + case airplane = "✈ī¸" + case smallAirplane = "🛩ī¸" + case airplaneDeparture = "đŸ›Ģ" + case airplaneArriving = "đŸ›Ŧ" + case parachute = "đŸĒ‚" + case seat = "đŸ’ē" + case helicopter = "🚁" + case suspensionRailway = "🚟" + case mountainCableway = "🚠" + case aerialTramway = "🚡" + case satellite = "🛰ī¸" + case rocket = "🚀" + case flyingSaucer = "🛸" + case bellhopBell = "🛎ī¸" + case luggage = "đŸ§ŗ" + case hourglass = "⌛" + case hourglassFlowingSand = "âŗ" + case watch = "⌚" + case alarmClock = "⏰" + case stopwatch = "⏱ī¸" + case timerClock = "⏲ī¸" + case mantelpieceClock = "🕰ī¸" + case clock12 = "🕛" + case clock1230 = "🕧" + case clock1 = "🕐" + case clock130 = "🕜" + case clock2 = "🕑" + case clock230 = "🕝" + case clock3 = "🕒" + case clock330 = "🕞" + case clock4 = "🕓" + case clock430 = "🕟" + case clock5 = "🕔" + case clock530 = "🕠" + case clock6 = "🕕" + case clock630 = "🕡" + case clock7 = "🕖" + case clock730 = "đŸ•ĸ" + case clock8 = "🕗" + case clock830 = "đŸ•Ŗ" + case clock9 = "🕘" + case clock930 = "🕤" + case clock10 = "🕙" + case clock1030 = "đŸ•Ĩ" + case clock11 = "🕚" + case clock1130 = "đŸ•Ļ" + case newMoon = "🌑" + case waxingCrescentMoon = "🌒" + case firstQuarterMoon = "🌓" + case moon = "🌔" + case fullMoon = "🌕" + case waningGibbousMoon = "🌖" + case lastQuarterMoon = "🌗" + case waningCrescentMoon = "🌘" + case crescentMoon = "🌙" + case newMoonWithFace = "🌚" + case firstQuarterMoonWithFace = "🌛" + case lastQuarterMoonWithFace = "🌜" + case thermometer = "🌡ī¸" + case sunny = "☀ī¸" + case fullMoonWithFace = "🌝" + case sunWithFace = "🌞" + case ringedPlanet = "đŸĒ" + case star = "⭐" + case star2 = "🌟" + case stars = "🌠" + case milkyWay = "🌌" + case cloud = "☁ī¸" + case partlySunny = "⛅" + case thunderCloudAndRain = "⛈ī¸" + case mostlySunny = "🌤ī¸" + case barelySunny = "đŸŒĨī¸" + case partlySunnyRain = "đŸŒĻī¸" + case rainCloud = "🌧ī¸" + case snowCloud = "🌨ī¸" + case lightning = "🌩ī¸" + case tornado = "đŸŒĒī¸" + case fog = "đŸŒĢī¸" + case windBlowingFace = "đŸŒŦī¸" + case cyclone = "🌀" + case rainbow = "🌈" + case closedUmbrella = "🌂" + case umbrella = "☂ī¸" + case umbrellaWithRainDrops = "☔" + case umbrellaOnGround = "⛱ī¸" + case zap = "⚡" + case snowflake = "❄ī¸" + case snowman = "☃ī¸" + case snowmanWithoutSnow = "⛄" + case comet = "☄ī¸" + case fire = "đŸ”Ĩ" + case droplet = "💧" + case ocean = "🌊" + case jackOLantern = "🎃" + case christmasTree = "🎄" + case fireworks = "🎆" + case sparkler = "🎇" + case firecracker = "🧨" + case sparkles = "✨" + case balloon = "🎈" + case tada = "🎉" + case confettiBall = "🎊" + case tanabataTree = "🎋" + case bamboo = "🎍" + case dolls = "🎎" + case flags = "🎏" + case windChime = "🎐" + case riceScene = "🎑" + case redEnvelope = "🧧" + case ribbon = "🎀" + case gift = "🎁" + case reminderRibbon = "🎗ī¸" + case admissionTickets = "🎟ī¸" + case ticket = "đŸŽĢ" + case medal = "🎖ī¸" + case trophy = "🏆" + case sportsMedal = "🏅" + case firstPlaceMedal = "đŸĨ‡" + case secondPlaceMedal = "đŸĨˆ" + case thirdPlaceMedal = "đŸĨ‰" + case soccer = "âšŊ" + case baseball = "⚾" + case softball = "đŸĨŽ" + case basketball = "🏀" + case volleyball = "🏐" + case football = "🏈" + case rugbyFootball = "🏉" + case tennis = "🎾" + case flyingDisc = "đŸĨ" + case bowling = "đŸŽŗ" + case cricketBatAndBall = "🏏" + case fieldHockeyStickAndBall = "🏑" + case iceHockeyStickAndPuck = "🏒" + case lacrosse = "đŸĨ" + case tableTennisPaddleAndBall = "🏓" + case badmintonRacquetAndShuttlecock = "🏸" + case boxingGlove = "đŸĨŠ" + case martialArtsUniform = "đŸĨ‹" + case goalNet = "đŸĨ…" + case golf = "â›ŗ" + case iceSkate = "⛸ī¸" + case fishingPoleAndFish = "đŸŽŖ" + case divingMask = "đŸ¤ŋ" + case runningShirtWithSash = "đŸŽŊ" + case ski = "đŸŽŋ" + case sled = "🛷" + case curlingStone = "đŸĨŒ" + case dart = "đŸŽ¯" + case yoYo = "đŸĒ€" + case kite = "đŸĒ" + case eightBall = "🎱" + case crystalBall = "🔮" + case magicWand = "đŸĒ„" + case nazarAmulet = "đŸ§ŋ" + case hamsa = "đŸĒŦ" + case videoGame = "🎮" + case joystick = "🕹ī¸" + case slotMachine = "🎰" + case gameDie = "🎲" + case jigsaw = "🧩" + case teddyBear = "🧸" + case pinata = "đŸĒ…" + case mirrorBall = "đŸĒŠ" + case nestingDolls = "đŸĒ†" + case spades = "♠ī¸" + case hearts = "â™Ĩī¸" + case diamonds = "â™Ļī¸" + case clubs = "â™Ŗī¸" + case chessPawn = "♟ī¸" + case blackJoker = "🃏" + case mahjong = "🀄" + case flowerPlayingCards = "🎴" + case performingArts = "🎭" + case frameWithPicture = "đŸ–ŧī¸" + case art = "🎨" + case thread = "đŸ§ĩ" + case sewingNeedle = "đŸĒĄ" + case yarn = "đŸ§ļ" + case knot = "đŸĒĸ" + case eyeglasses = "👓" + case darkSunglasses = "đŸ•ļī¸" + case goggles = "đŸĨŊ" + case labCoat = "đŸĨŧ" + case safetyVest = "đŸĻē" + case necktie = "👔" + case shirt = "👕" + case jeans = "👖" + case scarf = "đŸ§Ŗ" + case gloves = "🧤" + case coat = "đŸ§Ĩ" + case socks = "đŸ§Ļ" + case dress = "👗" + case kimono = "👘" + case sari = "đŸĨģ" + case onePieceSwimsuit = "🩱" + case briefs = "🩲" + case shorts = "đŸŠŗ" + case bikini = "👙" + case womansClothes = "👚" + case purse = "👛" + case handbag = "👜" + case pouch = "👝" + case shoppingBags = "🛍ī¸" + case schoolSatchel = "🎒" + case thongSandal = "🩴" + case mansShoe = "👞" + case athleticShoe = "👟" + case hikingBoot = "đŸĨž" + case womansFlatShoe = "đŸĨŋ" + case highHeel = "👠" + case sandal = "👡" + case balletShoes = "🩰" + case boot = "đŸ‘ĸ" + case crown = "👑" + case womansHat = "👒" + case tophat = "🎩" + case mortarBoard = "🎓" + case billedCap = "đŸ§ĸ" + case militaryHelmet = "đŸĒ–" + case helmetWithWhiteCross = "⛑ī¸" + case prayerBeads = "đŸ“ŋ" + case lipstick = "💄" + case ring = "💍" + case gem = "💎" + case mute = "🔇" + case speaker = "🔈" + case sound = "🔉" + case loudSound = "🔊" + case loudspeaker = "đŸ“ĸ" + case mega = "đŸ“Ŗ" + case postalHorn = "đŸ“¯" + case bell = "🔔" + case noBell = "🔕" + case musicalScore = "đŸŽŧ" + case musicalNote = "đŸŽĩ" + case notes = "đŸŽļ" + case studioMicrophone = "🎙ī¸" + case levelSlider = "🎚ī¸" + case controlKnobs = "🎛ī¸" + case microphone = "🎤" + case headphones = "🎧" + case radio = "đŸ“ģ" + case saxophone = "🎷" + case accordion = "đŸĒ—" + case guitar = "🎸" + case musicalKeyboard = "🎹" + case trumpet = "đŸŽē" + case violin = "đŸŽģ" + case banjo = "đŸĒ•" + case drumWithDrumsticks = "đŸĨ" + case longDrum = "đŸĒ˜" + case iphone = "📱" + case calling = "📲" + case phone = "☎ī¸" + case telephoneReceiver = "📞" + case pager = "📟" + case fax = "📠" + case battery = "🔋" + case lowBattery = "đŸĒĢ" + case electricPlug = "🔌" + case computer = "đŸ’ģ" + case desktopComputer = "đŸ–Ĩī¸" + case printer = "🖨ī¸" + case keyboard = "⌨ī¸" + case threeButtonMouse = "🖱ī¸" + case trackball = "🖲ī¸" + case minidisc = "đŸ’Ŋ" + case floppyDisk = "💾" + case cd = "đŸ’ŋ" + case dvd = "📀" + case abacus = "🧮" + case movieCamera = "đŸŽĨ" + case filmFrames = "🎞ī¸" + case filmProjector = "đŸ“Ŋī¸" + case clapper = "đŸŽŦ" + case tv = "đŸ“ē" + case camera = "📷" + case cameraWithFlash = "📸" + case videoCamera = "📹" + case vhs = "đŸ“ŧ" + case mag = "🔍" + case magRight = "🔎" + case candle = "đŸ•¯ī¸" + case bulb = "💡" + case flashlight = "đŸ”Ļ" + case izakayaLantern = "🏮" + case diyaLamp = "đŸĒ”" + case notebookWithDecorativeCover = "📔" + case closedBook = "📕" + case book = "📖" + case greenBook = "📗" + case blueBook = "📘" + case orangeBook = "📙" + case books = "📚" + case notebook = "📓" + case ledger = "📒" + case pageWithCurl = "📃" + case scroll = "📜" + case pageFacingUp = "📄" + case newspaper = "📰" + case rolledUpNewspaper = "🗞ī¸" + case bookmarkTabs = "📑" + case bookmark = "🔖" + case label = "🏷ī¸" + case moneybag = "💰" + case coin = "đŸĒ™" + case yen = "💴" + case dollar = "đŸ’ĩ" + case euro = "đŸ’ļ" + case pound = "💷" + case moneyWithWings = "💸" + case creditCard = "đŸ’ŗ" + case receipt = "🧾" + case chart = "💹" + case email = "✉ī¸" + case eMail = "📧" + case incomingEnvelope = "📨" + case envelopeWithArrow = "📩" + case outboxTray = "📤" + case inboxTray = "đŸ“Ĩ" + case package = "đŸ“Ļ" + case mailbox = "đŸ“Ģ" + case mailboxClosed = "đŸ“Ē" + case mailboxWithMail = "đŸ“Ŧ" + case mailboxWithNoMail = "📭" + case postbox = "📮" + case ballotBoxWithBallot = "đŸ—ŗī¸" + case pencil2 = "✏ī¸" + case blackNib = "✒ī¸" + case lowerLeftFountainPen = "🖋ī¸" + case lowerLeftBallpointPen = "🖊ī¸" + case lowerLeftPaintbrush = "🖌ī¸" + case lowerLeftCrayon = "🖍ī¸" + case memo = "📝" + case briefcase = "đŸ’ŧ" + case fileFolder = "📁" + case openFileFolder = "📂" + case cardIndexDividers = "🗂ī¸" + case date = "📅" + case calendar = "📆" + case spiralNotePad = "🗒ī¸" + case spiralCalendarPad = "🗓ī¸" + case cardIndex = "📇" + case chartWithUpwardsTrend = "📈" + case chartWithDownwardsTrend = "📉" + case barChart = "📊" + case clipboard = "📋" + case pushpin = "📌" + case roundPushpin = "📍" + case paperclip = "📎" + case linkedPaperclips = "🖇ī¸" + case straightRuler = "📏" + case triangularRuler = "📐" + case scissors = "✂ī¸" + case cardFileBox = "🗃ī¸" + case fileCabinet = "🗄ī¸" + case wastebasket = "🗑ī¸" + case lock = "🔒" + case unlock = "🔓" + case lockWithInkPen = "🔏" + case closedLockWithKey = "🔐" + case key = "🔑" + case oldKey = "🗝ī¸" + case hammer = "🔨" + case axe = "đŸĒ“" + case pick = "⛏ī¸" + case hammerAndPick = "⚒ī¸" + case hammerAndWrench = "🛠ī¸" + case daggerKnife = "🗡ī¸" + case crossedSwords = "⚔ī¸" + case gun = "đŸ”Ģ" + case boomerang = "đŸĒƒ" + case bowAndArrow = "🏹" + case shield = "🛡ī¸" + case carpentrySaw = "đŸĒš" + case wrench = "🔧" + case screwdriver = "đŸĒ›" + case nutAndBolt = "🔩" + case gear = "⚙ī¸" + case compression = "🗜ī¸" + case scales = "⚖ī¸" + case probingCane = "đŸĻ¯" + case link = "🔗" + case chains = "⛓ī¸" + case hook = "đŸĒ" + case toolbox = "🧰" + case magnet = "🧲" + case ladder = "đŸĒœ" + case alembic = "⚗ī¸" + case testTube = "đŸ§Ē" + case petriDish = "đŸ§Ģ" + case dna = "đŸ§Ŧ" + case microscope = "đŸ”Ŧ" + case telescope = "🔭" + case satelliteAntenna = "📡" + case syringe = "💉" + case dropOfBlood = "🩸" + case pill = "💊" + case adhesiveBandage = "🩹" + case crutch = "đŸŠŧ" + case stethoscope = "đŸŠē" + case xRay = "đŸŠģ" + case door = "đŸšĒ" + case elevator = "🛗" + case mirror = "đŸĒž" + case window = "đŸĒŸ" + case bed = "🛏ī¸" + case couchAndLamp = "🛋ī¸" + case chair = "đŸĒ‘" + case toilet = "đŸšŊ" + case plunger = "đŸĒ " + case shower = "đŸšŋ" + case bathtub = "🛁" + case mouseTrap = "đŸĒ¤" + case razor = "đŸĒ’" + case lotionBottle = "🧴" + case safetyPin = "🧷" + case broom = "🧹" + case basket = "đŸ§ē" + case rollOfPaper = "đŸ§ģ" + case bucket = "đŸĒŖ" + case soap = "đŸ§ŧ" + case bubbles = "đŸĢ§" + case toothbrush = "đŸĒĨ" + case sponge = "đŸ§Ŋ" + case fireExtinguisher = "đŸ§¯" + case shoppingTrolley = "🛒" + case smoking = "đŸšŦ" + case coffin = "⚰ī¸" + case headstone = "đŸĒĻ" + case funeralUrn = "⚱ī¸" + case moyai = "đŸ—ŋ" + case placard = "đŸĒ§" + case identificationCard = "đŸĒĒ" + case atm = "🏧" + case putLitterInItsPlace = "🚮" + case potableWater = "🚰" + case wheelchair = "â™ŋ" + case mens = "🚹" + case womens = "đŸšē" + case restroom = "đŸšģ" + case babySymbol = "đŸšŧ" + case wc = "🚾" + case passportControl = "🛂" + case customs = "🛃" + case baggageClaim = "🛄" + case leftLuggage = "🛅" + case warning = "⚠ī¸" + case childrenCrossing = "🚸" + case noEntry = "⛔" + case noEntrySign = "đŸšĢ" + case noBicycles = "đŸšŗ" + case noSmoking = "🚭" + case doNotLitter = "đŸš¯" + case nonPotableWater = "🚱" + case noPedestrians = "🚷" + case noMobilePhones = "đŸ“ĩ" + case underage = "🔞" + case radioactiveSign = "â˜ĸī¸" + case biohazardSign = "â˜Ŗī¸" + case arrowUp = "âŦ†ī¸" + case arrowUpperRight = "↗ī¸" + case arrowRight = "➡ī¸" + case arrowLowerRight = "↘ī¸" + case arrowDown = "âŦ‡ī¸" + case arrowLowerLeft = "↙ī¸" + case arrowLeft = "âŦ…ī¸" + case arrowUpperLeft = "↖ī¸" + case arrowUpDown = "↕ī¸" + case leftRightArrow = "↔ī¸" + case leftwardsArrowWithHook = "↩ī¸" + case arrowRightHook = "â†Ēī¸" + case arrowHeadingUp = "⤴ī¸" + case arrowHeadingDown = "â¤ĩī¸" + case arrowsClockwise = "🔃" + case arrowsCounterclockwise = "🔄" + case back = "🔙" + case end = "🔚" + case on = "🔛" + case soon = "🔜" + case top = "🔝" + case placeOfWorship = "🛐" + case atomSymbol = "⚛ī¸" + case omSymbol = "🕉ī¸" + case starOfDavid = "✡ī¸" + case wheelOfDharma = "☸ī¸" + case yinYang = "☯ī¸" + case latinCross = "✝ī¸" + case orthodoxCross = "â˜Ļī¸" + case starAndCrescent = "â˜Ēī¸" + case peaceSymbol = "☎ī¸" + case menorahWithNineBranches = "🕎" + case sixPointedStar = "đŸ”¯" + case aries = "♈" + case taurus = "♉" + case gemini = "♊" + case cancer = "♋" + case leo = "♌" + case virgo = "♍" + case libra = "♎" + case scorpius = "♏" + case sagittarius = "♐" + case capricorn = "♑" + case aquarius = "♒" + case pisces = "♓" + case ophiuchus = "⛎" + case twistedRightwardsArrows = "🔀" + case `repeat` = "🔁" + case repeatOne = "🔂" + case arrowForward = "â–ļī¸" + case fastForward = "⏊" + case blackRightPointingDoubleTriangleWithVerticalBar = "⏭ī¸" + case blackRightPointingTriangleWithDoubleVerticalBar = "⏯ī¸" + case arrowBackward = "◀ī¸" + case rewind = "âĒ" + case blackLeftPointingDoubleTriangleWithVerticalBar = "⏎ī¸" + case arrowUpSmall = "đŸ”ŧ" + case arrowDoubleUp = "âĢ" + case arrowDownSmall = "đŸ”Ŋ" + case arrowDoubleDown = "âŦ" + case doubleVerticalBar = "⏸ī¸" + case blackSquareForStop = "⏚ī¸" + case blackCircleForRecord = "âēī¸" + case eject = "⏏ī¸" + case cinema = "đŸŽĻ" + case lowBrightness = "🔅" + case highBrightness = "🔆" + case signalStrength = "đŸ“ļ" + case vibrationMode = "đŸ“ŗ" + case mobilePhoneOff = "📴" + case femaleSign = "♀ī¸" + case maleSign = "♂ī¸" + case transgenderSymbol = "⚧ī¸" + case heavyMultiplicationX = "✖ī¸" + case heavyPlusSign = "➕" + case heavyMinusSign = "➖" + case heavyDivisionSign = "➗" + case heavyEqualsSign = "🟰" + case infinity = "♾ī¸" + case bangbang = "â€ŧī¸" + case interrobang = "⁉ī¸" + case question = "❓" + case greyQuestion = "❔" + case greyExclamation = "❕" + case exclamation = "❗" + case wavyDash = "〰ī¸" + case currencyExchange = "💱" + case heavyDollarSign = "💲" + case medicalSymbol = "⚕ī¸" + case recycle = "â™ģī¸" + case fleurDeLis = "⚜ī¸" + case trident = "🔱" + case nameBadge = "📛" + case beginner = "🔰" + case o = "⭕" + case whiteCheckMark = "✅" + case ballotBoxWithCheck = "☑ī¸" + case heavyCheckMark = "✔ī¸" + case x = "❌" + case negativeSquaredCrossMark = "❎" + case curlyLoop = "➰" + case loop = "âžŋ" + case partAlternationMark = "ã€Ŋī¸" + case eightSpokedAsterisk = "âœŗī¸" + case eightPointedBlackStar = "✴ī¸" + case sparkle = "❇ī¸" + case copyright = "Šī¸" + case registered = "ÂŽī¸" + case tm = "â„ĸī¸" + case hash = "#ī¸âƒŖ" + case keycapStar = "*ī¸âƒŖ" + case zero = "0ī¸âƒŖ" + case one = "1ī¸âƒŖ" + case two = "2ī¸âƒŖ" + case three = "3ī¸âƒŖ" + case four = "4ī¸âƒŖ" + case five = "5ī¸âƒŖ" + case six = "6ī¸âƒŖ" + case seven = "7ī¸âƒŖ" + case eight = "8ī¸âƒŖ" + case nine = "9ī¸âƒŖ" + case keycapTen = "🔟" + case capitalAbcd = "🔠" + case abcd = "🔡" + case oneTwoThreeFour = "đŸ”ĸ" + case symbols = "đŸ”Ŗ" + case abc = "🔤" + case a = "🅰ī¸" + case ab = "🆎" + case b = "🅱ī¸" + case cl = "🆑" + case cool = "🆒" + case free = "🆓" + case informationSource = "ℹī¸" + case id = "🆔" + case m = "Ⓜī¸" + case new = "🆕" + case ng = "🆖" + case o2 = "🅾ī¸" + case ok = "🆗" + case parking = "đŸ…ŋī¸" + case sos = "🆘" + case up = "🆙" + case vs = "🆚" + case koko = "🈁" + case sa = "🈂ī¸" + case u6708 = "🈷ī¸" + case u6709 = "đŸˆļ" + case u6307 = "đŸˆ¯" + case ideographAdvantage = "🉐" + case u5272 = "🈹" + case u7121 = "🈚" + case u7981 = "🈲" + case accept = "🉑" + case u7533 = "🈸" + case u5408 = "🈴" + case u7a7a = "đŸˆŗ" + case congratulations = "㊗ī¸" + case secret = "㊙ī¸" + case u55b6 = "đŸˆē" + case u6e80 = "đŸˆĩ" + case redCircle = "🔴" + case largeOrangeCircle = "🟠" + case largeYellowCircle = "🟡" + case largeGreenCircle = "đŸŸĸ" + case largeBlueCircle = "đŸ”ĩ" + case largePurpleCircle = "đŸŸŖ" + case largeBrownCircle = "🟤" + case blackCircle = "âšĢ" + case whiteCircle = "âšĒ" + case largeRedSquare = "đŸŸĨ" + case largeOrangeSquare = "🟧" + case largeYellowSquare = "🟨" + case largeGreenSquare = "🟩" + case largeBlueSquare = "đŸŸĻ" + case largePurpleSquare = "đŸŸĒ" + case largeBrownSquare = "đŸŸĢ" + case blackLargeSquare = "âŦ›" + case whiteLargeSquare = "âŦœ" + case blackMediumSquare = "â—ŧī¸" + case whiteMediumSquare = "â—ģī¸" + case blackMediumSmallSquare = "◾" + case whiteMediumSmallSquare = "â—Ŋ" + case blackSmallSquare = "â–Ēī¸" + case whiteSmallSquare = "â–Ģī¸" + case largeOrangeDiamond = "đŸ”ļ" + case largeBlueDiamond = "🔷" + case smallOrangeDiamond = "🔸" + case smallBlueDiamond = "🔹" + case smallRedTriangle = "đŸ”ē" + case smallRedTriangleDown = "đŸ”ģ" + case diamondShapeWithADotInside = "💠" + case radioButton = "🔘" + case whiteSquareButton = "đŸ”ŗ" + case blackSquareButton = "🔲" + case checkeredFlag = "🏁" + case triangularFlagOnPost = "🚩" + case crossedFlags = "🎌" + case wavingBlackFlag = "🏴" + case wavingWhiteFlag = "đŸŗī¸" + case rainbowFlag = "đŸŗī¸â€đŸŒˆ" + case transgenderFlag = "đŸŗī¸â€âš§ī¸" + case pirateFlag = "🏴‍☠ī¸" + case flagAc = "đŸ‡Ļ🇨" + case flagAd = "đŸ‡Ļ🇩" + case flagAe = "đŸ‡ĻđŸ‡Ē" + case flagAf = "đŸ‡ĻđŸ‡Ģ" + case flagAg = "đŸ‡ĻđŸ‡Ŧ" + case flagAi = "đŸ‡Ļ🇮" + case flagAl = "đŸ‡Ļ🇱" + case flagAm = "đŸ‡Ļ🇲" + case flagAo = "đŸ‡Ļ🇴" + case flagAq = "đŸ‡ĻđŸ‡ļ" + case flagAr = "đŸ‡Ļ🇷" + case flagAs = "đŸ‡Ļ🇸" + case flagAt = "đŸ‡Ļ🇹" + case flagAu = "đŸ‡ĻđŸ‡ē" + case flagAw = "đŸ‡ĻđŸ‡ŧ" + case flagAx = "đŸ‡ĻđŸ‡Ŋ" + case flagAz = "đŸ‡ĻđŸ‡ŋ" + case flagBa = "🇧đŸ‡Ļ" + case flagBb = "🇧🇧" + case flagBd = "🇧🇩" + case flagBe = "🇧đŸ‡Ē" + case flagBf = "🇧đŸ‡Ģ" + case flagBg = "🇧đŸ‡Ŧ" + case flagBh = "🇧🇭" + case flagBi = "🇧🇮" + case flagBj = "đŸ‡§đŸ‡¯" + case flagBl = "🇧🇱" + case flagBm = "🇧🇲" + case flagBn = "🇧đŸ‡ŗ" + case flagBo = "🇧🇴" + case flagBq = "🇧đŸ‡ļ" + case flagBr = "🇧🇷" + case flagBs = "🇧🇸" + case flagBt = "🇧🇹" + case flagBv = "🇧đŸ‡ģ" + case flagBw = "🇧đŸ‡ŧ" + case flagBy = "🇧🇾" + case flagBz = "🇧đŸ‡ŋ" + case flagCa = "🇨đŸ‡Ļ" + case flagCc = "🇨🇨" + case flagCd = "🇨🇩" + case flagCf = "🇨đŸ‡Ģ" + case flagCg = "🇨đŸ‡Ŧ" + case flagCh = "🇨🇭" + case flagCi = "🇨🇮" + case flagCk = "🇨🇰" + case flagCl = "🇨🇱" + case flagCm = "🇨🇲" + case cn = "🇨đŸ‡ŗ" + case flagCo = "🇨🇴" + case flagCp = "🇨đŸ‡ĩ" + case flagCr = "🇨🇷" + case flagCu = "🇨đŸ‡ē" + case flagCv = "🇨đŸ‡ģ" + case flagCw = "🇨đŸ‡ŧ" + case flagCx = "🇨đŸ‡Ŋ" + case flagCy = "🇨🇾" + case flagCz = "🇨đŸ‡ŋ" + case de = "🇩đŸ‡Ē" + case flagDg = "🇩đŸ‡Ŧ" + case flagDj = "đŸ‡ŠđŸ‡¯" + case flagDk = "🇩🇰" + case flagDm = "🇩🇲" + case flagDo = "🇩🇴" + case flagDz = "🇩đŸ‡ŋ" + case flagEa = "đŸ‡ĒđŸ‡Ļ" + case flagEc = "đŸ‡Ē🇨" + case flagEe = "đŸ‡ĒđŸ‡Ē" + case flagEg = "đŸ‡ĒđŸ‡Ŧ" + case flagEh = "đŸ‡Ē🇭" + case flagEr = "đŸ‡Ē🇷" + case es = "đŸ‡Ē🇸" + case flagEt = "đŸ‡Ē🇹" + case flagEu = "đŸ‡ĒđŸ‡ē" + case flagFi = "đŸ‡Ģ🇮" + case flagFj = "đŸ‡ĢđŸ‡¯" + case flagFk = "đŸ‡Ģ🇰" + case flagFm = "đŸ‡Ģ🇲" + case flagFo = "đŸ‡Ģ🇴" + case fr = "đŸ‡Ģ🇷" + case flagGa = "đŸ‡ŦđŸ‡Ļ" + case gb = "đŸ‡Ŧ🇧" + case flagGd = "đŸ‡Ŧ🇩" + case flagGe = "đŸ‡ŦđŸ‡Ē" + case flagGf = "đŸ‡ŦđŸ‡Ģ" + case flagGg = "đŸ‡ŦđŸ‡Ŧ" + case flagGh = "đŸ‡Ŧ🇭" + case flagGi = "đŸ‡Ŧ🇮" + case flagGl = "đŸ‡Ŧ🇱" + case flagGm = "đŸ‡Ŧ🇲" + case flagGn = "đŸ‡ŦđŸ‡ŗ" + case flagGp = "đŸ‡ŦđŸ‡ĩ" + case flagGq = "đŸ‡ŦđŸ‡ļ" + case flagGr = "đŸ‡Ŧ🇷" + case flagGs = "đŸ‡Ŧ🇸" + case flagGt = "đŸ‡Ŧ🇹" + case flagGu = "đŸ‡ŦđŸ‡ē" + case flagGw = "đŸ‡ŦđŸ‡ŧ" + case flagGy = "đŸ‡Ŧ🇾" + case flagHk = "🇭🇰" + case flagHm = "🇭🇲" + case flagHn = "🇭đŸ‡ŗ" + case flagHr = "🇭🇷" + case flagHt = "🇭🇹" + case flagHu = "🇭đŸ‡ē" + case flagIc = "🇮🇨" + case flagId = "🇮🇩" + case flagIe = "🇮đŸ‡Ē" + case flagIl = "🇮🇱" + case flagIm = "🇮🇲" + case flagIn = "🇮đŸ‡ŗ" + case flagIo = "🇮🇴" + case flagIq = "🇮đŸ‡ļ" + case flagIr = "🇮🇷" + case flagIs = "🇮🇸" + case it = "🇮🇹" + case flagJe = "đŸ‡¯đŸ‡Ē" + case flagJm = "đŸ‡¯đŸ‡˛" + case flagJo = "đŸ‡¯đŸ‡´" + case jp = "đŸ‡¯đŸ‡ĩ" + case flagKe = "🇰đŸ‡Ē" + case flagKg = "🇰đŸ‡Ŧ" + case flagKh = "🇰🇭" + case flagKi = "🇰🇮" + case flagKm = "🇰🇲" + case flagKn = "🇰đŸ‡ŗ" + case flagKp = "🇰đŸ‡ĩ" + case kr = "🇰🇷" + case flagKw = "🇰đŸ‡ŧ" + case flagKy = "🇰🇾" + case flagKz = "🇰đŸ‡ŋ" + case flagLa = "🇱đŸ‡Ļ" + case flagLb = "🇱🇧" + case flagLc = "🇱🇨" + case flagLi = "🇱🇮" + case flagLk = "🇱🇰" + case flagLr = "🇱🇷" + case flagLs = "🇱🇸" + case flagLt = "🇱🇹" + case flagLu = "🇱đŸ‡ē" + case flagLv = "🇱đŸ‡ģ" + case flagLy = "🇱🇾" + case flagMa = "🇲đŸ‡Ļ" + case flagMc = "🇲🇨" + case flagMd = "🇲🇩" + case flagMe = "🇲đŸ‡Ē" + case flagMf = "🇲đŸ‡Ģ" + case flagMg = "🇲đŸ‡Ŧ" + case flagMh = "🇲🇭" + case flagMk = "🇲🇰" + case flagMl = "🇲🇱" + case flagMm = "🇲🇲" + case flagMn = "🇲đŸ‡ŗ" + case flagMo = "🇲🇴" + case flagMp = "🇲đŸ‡ĩ" + case flagMq = "🇲đŸ‡ļ" + case flagMr = "🇲🇷" + case flagMs = "🇲🇸" + case flagMt = "🇲🇹" + case flagMu = "🇲đŸ‡ē" + case flagMv = "🇲đŸ‡ģ" + case flagMw = "🇲đŸ‡ŧ" + case flagMx = "🇲đŸ‡Ŋ" + case flagMy = "🇲🇾" + case flagMz = "🇲đŸ‡ŋ" + case flagNa = "đŸ‡ŗđŸ‡Ļ" + case flagNc = "đŸ‡ŗ🇨" + case flagNe = "đŸ‡ŗđŸ‡Ē" + case flagNf = "đŸ‡ŗđŸ‡Ģ" + case flagNg = "đŸ‡ŗđŸ‡Ŧ" + case flagNi = "đŸ‡ŗ🇮" + case flagNl = "đŸ‡ŗ🇱" + case flagNo = "đŸ‡ŗ🇴" + case flagNp = "đŸ‡ŗđŸ‡ĩ" + case flagNr = "đŸ‡ŗ🇷" + case flagNu = "đŸ‡ŗđŸ‡ē" + case flagNz = "đŸ‡ŗđŸ‡ŋ" + case flagOm = "🇴🇲" + case flagPa = "đŸ‡ĩđŸ‡Ļ" + case flagPe = "đŸ‡ĩđŸ‡Ē" + case flagPf = "đŸ‡ĩđŸ‡Ģ" + case flagPg = "đŸ‡ĩđŸ‡Ŧ" + case flagPh = "đŸ‡ĩ🇭" + case flagPk = "đŸ‡ĩ🇰" + case flagPl = "đŸ‡ĩ🇱" + case flagPm = "đŸ‡ĩ🇲" + case flagPn = "đŸ‡ĩđŸ‡ŗ" + case flagPr = "đŸ‡ĩ🇷" + case flagPs = "đŸ‡ĩ🇸" + case flagPt = "đŸ‡ĩ🇹" + case flagPw = "đŸ‡ĩđŸ‡ŧ" + case flagPy = "đŸ‡ĩ🇾" + case flagQa = "đŸ‡ļđŸ‡Ļ" + case flagRe = "🇷đŸ‡Ē" + case flagRo = "🇷🇴" + case flagRs = "🇷🇸" + case ru = "🇷đŸ‡ē" + case flagRw = "🇷đŸ‡ŧ" + case flagSa = "🇸đŸ‡Ļ" + case flagSb = "🇸🇧" + case flagSc = "🇸🇨" + case flagSd = "🇸🇩" + case flagSe = "🇸đŸ‡Ē" + case flagSg = "🇸đŸ‡Ŧ" + case flagSh = "🇸🇭" + case flagSi = "🇸🇮" + case flagSj = "đŸ‡¸đŸ‡¯" + case flagSk = "🇸🇰" + case flagSl = "🇸🇱" + case flagSm = "🇸🇲" + case flagSn = "🇸đŸ‡ŗ" + case flagSo = "🇸🇴" + case flagSr = "🇸🇷" + case flagSs = "🇸🇸" + case flagSt = "🇸🇹" + case flagSv = "🇸đŸ‡ģ" + case flagSx = "🇸đŸ‡Ŋ" + case flagSy = "🇸🇾" + case flagSz = "🇸đŸ‡ŋ" + case flagTa = "🇹đŸ‡Ļ" + case flagTc = "🇹🇨" + case flagTd = "🇹🇩" + case flagTf = "🇹đŸ‡Ģ" + case flagTg = "🇹đŸ‡Ŧ" + case flagTh = "🇹🇭" + case flagTj = "đŸ‡šđŸ‡¯" + case flagTk = "🇹🇰" + case flagTl = "🇹🇱" + case flagTm = "🇹🇲" + case flagTn = "🇹đŸ‡ŗ" + case flagTo = "🇹🇴" + case flagTr = "🇹🇷" + case flagTt = "🇹🇹" + case flagTv = "🇹đŸ‡ģ" + case flagTw = "🇹đŸ‡ŧ" + case flagTz = "🇹đŸ‡ŋ" + case flagUa = "đŸ‡ēđŸ‡Ļ" + case flagUg = "đŸ‡ēđŸ‡Ŧ" + case flagUm = "đŸ‡ē🇲" + case flagUn = "đŸ‡ēđŸ‡ŗ" + case us = "đŸ‡ē🇸" + case flagUy = "đŸ‡ē🇾" + case flagUz = "đŸ‡ēđŸ‡ŋ" + case flagVa = "đŸ‡ģđŸ‡Ļ" + case flagVc = "đŸ‡ģ🇨" + case flagVe = "đŸ‡ģđŸ‡Ē" + case flagVg = "đŸ‡ģđŸ‡Ŧ" + case flagVi = "đŸ‡ģ🇮" + case flagVn = "đŸ‡ģđŸ‡ŗ" + case flagVu = "đŸ‡ģđŸ‡ē" + case flagWf = "đŸ‡ŧđŸ‡Ģ" + case flagWs = "đŸ‡ŧ🇸" + case flagXk = "đŸ‡Ŋ🇰" + case flagYe = "🇾đŸ‡Ē" + case flagYt = "🇾🇹" + case flagZa = "đŸ‡ŋđŸ‡Ļ" + case flagZm = "đŸ‡ŋ🇲" + case flagZw = "đŸ‡ŋđŸ‡ŧ" + case flagEngland = "🏴ķ §ķ ĸķ Ĩķ Žķ §ķ ŋ" + case flagScotland = "🏴ķ §ķ ĸķ ŗķ Ŗķ ´ķ ŋ" + case flagWales = "🏴ķ §ķ ĸķ ˇķ Ŧķ ŗķ ŋ" +} +// swiftlint:disable all diff --git a/Session/Emoji/EmojiWithSkinTones+String.swift b/Session/Emoji/EmojiWithSkinTones+String.swift new file mode 100644 index 000000000..218048c86 --- /dev/null +++ b/Session/Emoji/EmojiWithSkinTones+String.swift @@ -0,0 +1,7269 @@ + +// This file is generated by EmojiGenerator.swift, do not manually edit it. + +extension EmojiWithSkinTones { + init?(rawValue: String) { + guard rawValue.isSingleEmoji else { return nil } + if rawValue == "😀" { + self.init(baseEmoji: .grinning, skinTones: nil) + } else if rawValue == "😃" { + self.init(baseEmoji: .smiley, skinTones: nil) + } else if rawValue == "😄" { + self.init(baseEmoji: .smile, skinTones: nil) + } else if rawValue == "😁" { + self.init(baseEmoji: .grin, skinTones: nil) + } else if rawValue == "😆" { + self.init(baseEmoji: .laughing, skinTones: nil) + } else if rawValue == "😅" { + self.init(baseEmoji: .sweatSmile, skinTones: nil) + } else if rawValue == "đŸ¤Ŗ" { + self.init(baseEmoji: .rollingOnTheFloorLaughing, skinTones: nil) + } else if rawValue == "😂" { + self.init(baseEmoji: .joy, skinTones: nil) + } else if rawValue == "🙂" { + self.init(baseEmoji: .slightlySmilingFace, skinTones: nil) + } else if rawValue == "🙃" { + self.init(baseEmoji: .upsideDownFace, skinTones: nil) + } else if rawValue == "đŸĢ " { + self.init(baseEmoji: .meltingFace, skinTones: nil) + } else if rawValue == "😉" { + self.init(baseEmoji: .wink, skinTones: nil) + } else if rawValue == "😊" { + self.init(baseEmoji: .blush, skinTones: nil) + } else if rawValue == "😇" { + self.init(baseEmoji: .innocent, skinTones: nil) + } else if rawValue == "đŸĨ°" { + self.init(baseEmoji: .smilingFaceWith3Hearts, skinTones: nil) + } else if rawValue == "😍" { + self.init(baseEmoji: .heartEyes, skinTones: nil) + } else if rawValue == "🤩" { + self.init(baseEmoji: .starStruck, skinTones: nil) + } else if rawValue == "😘" { + self.init(baseEmoji: .kissingHeart, skinTones: nil) + } else if rawValue == "😗" { + self.init(baseEmoji: .kissing, skinTones: nil) + } else if rawValue == "â˜ēī¸" { + self.init(baseEmoji: .relaxed, skinTones: nil) + } else if rawValue == "😚" { + self.init(baseEmoji: .kissingClosedEyes, skinTones: nil) + } else if rawValue == "😙" { + self.init(baseEmoji: .kissingSmilingEyes, skinTones: nil) + } else if rawValue == "đŸĨ˛" { + self.init(baseEmoji: .smilingFaceWithTear, skinTones: nil) + } else if rawValue == "😋" { + self.init(baseEmoji: .yum, skinTones: nil) + } else if rawValue == "😛" { + self.init(baseEmoji: .stuckOutTongue, skinTones: nil) + } else if rawValue == "😜" { + self.init(baseEmoji: .stuckOutTongueWinkingEye, skinTones: nil) + } else if rawValue == "đŸ¤Ē" { + self.init(baseEmoji: .zanyFace, skinTones: nil) + } else if rawValue == "😝" { + self.init(baseEmoji: .stuckOutTongueClosedEyes, skinTones: nil) + } else if rawValue == "🤑" { + self.init(baseEmoji: .moneyMouthFace, skinTones: nil) + } else if rawValue == "🤗" { + self.init(baseEmoji: .huggingFace, skinTones: nil) + } else if rawValue == "🤭" { + self.init(baseEmoji: .faceWithHandOverMouth, skinTones: nil) + } else if rawValue == "đŸĢĸ" { + self.init(baseEmoji: .faceWithOpenEyesAndHandOverMouth, skinTones: nil) + } else if rawValue == "đŸĢŖ" { + self.init(baseEmoji: .faceWithPeekingEye, skinTones: nil) + } else if rawValue == "đŸ¤Ģ" { + self.init(baseEmoji: .shushingFace, skinTones: nil) + } else if rawValue == "🤔" { + self.init(baseEmoji: .thinkingFace, skinTones: nil) + } else if rawValue == "đŸĢĄ" { + self.init(baseEmoji: .salutingFace, skinTones: nil) + } else if rawValue == "🤐" { + self.init(baseEmoji: .zipperMouthFace, skinTones: nil) + } else if rawValue == "🤨" { + self.init(baseEmoji: .faceWithRaisedEyebrow, skinTones: nil) + } else if rawValue == "😐" { + self.init(baseEmoji: .neutralFace, skinTones: nil) + } else if rawValue == "😑" { + self.init(baseEmoji: .expressionless, skinTones: nil) + } else if rawValue == "đŸ˜ļ" { + self.init(baseEmoji: .noMouth, skinTones: nil) + } else if rawValue == "đŸĢĨ" { + self.init(baseEmoji: .dottedLineFace, skinTones: nil) + } else if rawValue == "đŸ˜ļ‍đŸŒĢī¸" { + self.init(baseEmoji: .faceInClouds, skinTones: nil) + } else if rawValue == "😏" { + self.init(baseEmoji: .smirk, skinTones: nil) + } else if rawValue == "😒" { + self.init(baseEmoji: .unamused, skinTones: nil) + } else if rawValue == "🙄" { + self.init(baseEmoji: .faceWithRollingEyes, skinTones: nil) + } else if rawValue == "đŸ˜Ŧ" { + self.init(baseEmoji: .grimacing, skinTones: nil) + } else if rawValue == "😮‍💨" { + self.init(baseEmoji: .faceExhaling, skinTones: nil) + } else if rawValue == "đŸ¤Ĩ" { + self.init(baseEmoji: .lyingFace, skinTones: nil) + } else if rawValue == "😌" { + self.init(baseEmoji: .relieved, skinTones: nil) + } else if rawValue == "😔" { + self.init(baseEmoji: .pensive, skinTones: nil) + } else if rawValue == "đŸ˜Ē" { + self.init(baseEmoji: .sleepy, skinTones: nil) + } else if rawValue == "🤤" { + self.init(baseEmoji: .droolingFace, skinTones: nil) + } else if rawValue == "😴" { + self.init(baseEmoji: .sleeping, skinTones: nil) + } else if rawValue == "😷" { + self.init(baseEmoji: .mask, skinTones: nil) + } else if rawValue == "🤒" { + self.init(baseEmoji: .faceWithThermometer, skinTones: nil) + } else if rawValue == "🤕" { + self.init(baseEmoji: .faceWithHeadBandage, skinTones: nil) + } else if rawValue == "đŸ¤ĸ" { + self.init(baseEmoji: .nauseatedFace, skinTones: nil) + } else if rawValue == "🤮" { + self.init(baseEmoji: .faceVomiting, skinTones: nil) + } else if rawValue == "🤧" { + self.init(baseEmoji: .sneezingFace, skinTones: nil) + } else if rawValue == "đŸĨĩ" { + self.init(baseEmoji: .hotFace, skinTones: nil) + } else if rawValue == "đŸĨļ" { + self.init(baseEmoji: .coldFace, skinTones: nil) + } else if rawValue == "đŸĨ´" { + self.init(baseEmoji: .woozyFace, skinTones: nil) + } else if rawValue == "đŸ˜ĩ" { + self.init(baseEmoji: .dizzyFace, skinTones: nil) + } else if rawValue == "đŸ˜ĩ‍đŸ’Ģ" { + self.init(baseEmoji: .faceWithSpiralEyes, skinTones: nil) + } else if rawValue == "đŸ¤¯" { + self.init(baseEmoji: .explodingHead, skinTones: nil) + } else if rawValue == "🤠" { + self.init(baseEmoji: .faceWithCowboyHat, skinTones: nil) + } else if rawValue == "đŸĨŗ" { + self.init(baseEmoji: .partyingFace, skinTones: nil) + } else if rawValue == "đŸĨ¸" { + self.init(baseEmoji: .disguisedFace, skinTones: nil) + } else if rawValue == "😎" { + self.init(baseEmoji: .sunglasses, skinTones: nil) + } else if rawValue == "🤓" { + self.init(baseEmoji: .nerdFace, skinTones: nil) + } else if rawValue == "🧐" { + self.init(baseEmoji: .faceWithMonocle, skinTones: nil) + } else if rawValue == "😕" { + self.init(baseEmoji: .confused, skinTones: nil) + } else if rawValue == "đŸĢ¤" { + self.init(baseEmoji: .faceWithDiagonalMouth, skinTones: nil) + } else if rawValue == "😟" { + self.init(baseEmoji: .worried, skinTones: nil) + } else if rawValue == "🙁" { + self.init(baseEmoji: .slightlyFrowningFace, skinTones: nil) + } else if rawValue == "☚ī¸" { + self.init(baseEmoji: .whiteFrowningFace, skinTones: nil) + } else if rawValue == "😮" { + self.init(baseEmoji: .openMouth, skinTones: nil) + } else if rawValue == "đŸ˜¯" { + self.init(baseEmoji: .hushed, skinTones: nil) + } else if rawValue == "😲" { + self.init(baseEmoji: .astonished, skinTones: nil) + } else if rawValue == "đŸ˜ŗ" { + self.init(baseEmoji: .flushed, skinTones: nil) + } else if rawValue == "đŸĨē" { + self.init(baseEmoji: .pleadingFace, skinTones: nil) + } else if rawValue == "đŸĨš" { + self.init(baseEmoji: .faceHoldingBackTears, skinTones: nil) + } else if rawValue == "đŸ˜Ļ" { + self.init(baseEmoji: .frowning, skinTones: nil) + } else if rawValue == "😧" { + self.init(baseEmoji: .anguished, skinTones: nil) + } else if rawValue == "😨" { + self.init(baseEmoji: .fearful, skinTones: nil) + } else if rawValue == "😰" { + self.init(baseEmoji: .coldSweat, skinTones: nil) + } else if rawValue == "đŸ˜Ĩ" { + self.init(baseEmoji: .disappointedRelieved, skinTones: nil) + } else if rawValue == "đŸ˜ĸ" { + self.init(baseEmoji: .cry, skinTones: nil) + } else if rawValue == "😭" { + self.init(baseEmoji: .sob, skinTones: nil) + } else if rawValue == "😱" { + self.init(baseEmoji: .scream, skinTones: nil) + } else if rawValue == "😖" { + self.init(baseEmoji: .confounded, skinTones: nil) + } else if rawValue == "đŸ˜Ŗ" { + self.init(baseEmoji: .persevere, skinTones: nil) + } else if rawValue == "😞" { + self.init(baseEmoji: .disappointed, skinTones: nil) + } else if rawValue == "😓" { + self.init(baseEmoji: .sweat, skinTones: nil) + } else if rawValue == "😩" { + self.init(baseEmoji: .weary, skinTones: nil) + } else if rawValue == "đŸ˜Ģ" { + self.init(baseEmoji: .tiredFace, skinTones: nil) + } else if rawValue == "đŸĨą" { + self.init(baseEmoji: .yawningFace, skinTones: nil) + } else if rawValue == "😤" { + self.init(baseEmoji: .triumph, skinTones: nil) + } else if rawValue == "😡" { + self.init(baseEmoji: .rage, skinTones: nil) + } else if rawValue == "😠" { + self.init(baseEmoji: .angry, skinTones: nil) + } else if rawValue == "đŸ¤Ŧ" { + self.init(baseEmoji: .faceWithSymbolsOnMouth, skinTones: nil) + } else if rawValue == "😈" { + self.init(baseEmoji: .smilingImp, skinTones: nil) + } else if rawValue == "đŸ‘ŋ" { + self.init(baseEmoji: .imp, skinTones: nil) + } else if rawValue == "💀" { + self.init(baseEmoji: .skull, skinTones: nil) + } else if rawValue == "☠ī¸" { + self.init(baseEmoji: .skullAndCrossbones, skinTones: nil) + } else if rawValue == "💩" { + self.init(baseEmoji: .hankey, skinTones: nil) + } else if rawValue == "🤡" { + self.init(baseEmoji: .clownFace, skinTones: nil) + } else if rawValue == "👹" { + self.init(baseEmoji: .japaneseOgre, skinTones: nil) + } else if rawValue == "đŸ‘ē" { + self.init(baseEmoji: .japaneseGoblin, skinTones: nil) + } else if rawValue == "đŸ‘ģ" { + self.init(baseEmoji: .ghost, skinTones: nil) + } else if rawValue == "đŸ‘Ŋ" { + self.init(baseEmoji: .alien, skinTones: nil) + } else if rawValue == "👾" { + self.init(baseEmoji: .spaceInvader, skinTones: nil) + } else if rawValue == "🤖" { + self.init(baseEmoji: .robotFace, skinTones: nil) + } else if rawValue == "đŸ˜ē" { + self.init(baseEmoji: .smileyCat, skinTones: nil) + } else if rawValue == "😸" { + self.init(baseEmoji: .smileCat, skinTones: nil) + } else if rawValue == "😹" { + self.init(baseEmoji: .joyCat, skinTones: nil) + } else if rawValue == "đŸ˜ģ" { + self.init(baseEmoji: .heartEyesCat, skinTones: nil) + } else if rawValue == "đŸ˜ŧ" { + self.init(baseEmoji: .smirkCat, skinTones: nil) + } else if rawValue == "đŸ˜Ŋ" { + self.init(baseEmoji: .kissingCat, skinTones: nil) + } else if rawValue == "🙀" { + self.init(baseEmoji: .screamCat, skinTones: nil) + } else if rawValue == "đŸ˜ŋ" { + self.init(baseEmoji: .cryingCatFace, skinTones: nil) + } else if rawValue == "😾" { + self.init(baseEmoji: .poutingCat, skinTones: nil) + } else if rawValue == "🙈" { + self.init(baseEmoji: .seeNoEvil, skinTones: nil) + } else if rawValue == "🙉" { + self.init(baseEmoji: .hearNoEvil, skinTones: nil) + } else if rawValue == "🙊" { + self.init(baseEmoji: .speakNoEvil, skinTones: nil) + } else if rawValue == "💋" { + self.init(baseEmoji: .kiss, skinTones: nil) + } else if rawValue == "💌" { + self.init(baseEmoji: .loveLetter, skinTones: nil) + } else if rawValue == "💘" { + self.init(baseEmoji: .cupid, skinTones: nil) + } else if rawValue == "💝" { + self.init(baseEmoji: .giftHeart, skinTones: nil) + } else if rawValue == "💖" { + self.init(baseEmoji: .sparklingHeart, skinTones: nil) + } else if rawValue == "💗" { + self.init(baseEmoji: .heartpulse, skinTones: nil) + } else if rawValue == "💓" { + self.init(baseEmoji: .heartbeat, skinTones: nil) + } else if rawValue == "💞" { + self.init(baseEmoji: .revolvingHearts, skinTones: nil) + } else if rawValue == "💕" { + self.init(baseEmoji: .twoHearts, skinTones: nil) + } else if rawValue == "💟" { + self.init(baseEmoji: .heartDecoration, skinTones: nil) + } else if rawValue == "âŖī¸" { + self.init(baseEmoji: .heavyHeartExclamationMarkOrnament, skinTones: nil) + } else if rawValue == "💔" { + self.init(baseEmoji: .brokenHeart, skinTones: nil) + } else if rawValue == "❤ī¸â€đŸ”Ĩ" { + self.init(baseEmoji: .heartOnFire, skinTones: nil) + } else if rawValue == "❤ī¸â€đŸŠš" { + self.init(baseEmoji: .mendingHeart, skinTones: nil) + } else if rawValue == "❤ī¸" { + self.init(baseEmoji: .heart, skinTones: nil) + } else if rawValue == "🧡" { + self.init(baseEmoji: .orangeHeart, skinTones: nil) + } else if rawValue == "💛" { + self.init(baseEmoji: .yellowHeart, skinTones: nil) + } else if rawValue == "💚" { + self.init(baseEmoji: .greenHeart, skinTones: nil) + } else if rawValue == "💙" { + self.init(baseEmoji: .blueHeart, skinTones: nil) + } else if rawValue == "💜" { + self.init(baseEmoji: .purpleHeart, skinTones: nil) + } else if rawValue == "🤎" { + self.init(baseEmoji: .brownHeart, skinTones: nil) + } else if rawValue == "🖤" { + self.init(baseEmoji: .blackHeart, skinTones: nil) + } else if rawValue == "🤍" { + self.init(baseEmoji: .whiteHeart, skinTones: nil) + } else if rawValue == "đŸ’¯" { + self.init(baseEmoji: .oneHundred, skinTones: nil) + } else if rawValue == "đŸ’ĸ" { + self.init(baseEmoji: .anger, skinTones: nil) + } else if rawValue == "đŸ’Ĩ" { + self.init(baseEmoji: .boom, skinTones: nil) + } else if rawValue == "đŸ’Ģ" { + self.init(baseEmoji: .dizzy, skinTones: nil) + } else if rawValue == "đŸ’Ļ" { + self.init(baseEmoji: .sweatDrops, skinTones: nil) + } else if rawValue == "💨" { + self.init(baseEmoji: .dash, skinTones: nil) + } else if rawValue == "đŸ•ŗī¸" { + self.init(baseEmoji: .hole, skinTones: nil) + } else if rawValue == "đŸ’Ŗ" { + self.init(baseEmoji: .bomb, skinTones: nil) + } else if rawValue == "đŸ’Ŧ" { + self.init(baseEmoji: .speechBalloon, skinTones: nil) + } else if rawValue == "👁ī¸â€đŸ—¨ī¸" { + self.init(baseEmoji: .eyeInSpeechBubble, skinTones: nil) + } else if rawValue == "🗨ī¸" { + self.init(baseEmoji: .leftSpeechBubble, skinTones: nil) + } else if rawValue == "đŸ—¯ī¸" { + self.init(baseEmoji: .rightAngerBubble, skinTones: nil) + } else if rawValue == "💭" { + self.init(baseEmoji: .thoughtBalloon, skinTones: nil) + } else if rawValue == "💤" { + self.init(baseEmoji: .zzz, skinTones: nil) + } else if rawValue == "👋" { + self.init(baseEmoji: .wave, skinTones: nil) + } else if rawValue == "👋đŸģ" { + self.init(baseEmoji: .wave, skinTones: [.light]) + } else if rawValue == "👋đŸŧ" { + self.init(baseEmoji: .wave, skinTones: [.mediumLight]) + } else if rawValue == "👋đŸŊ" { + self.init(baseEmoji: .wave, skinTones: [.medium]) + } else if rawValue == "👋🏾" { + self.init(baseEmoji: .wave, skinTones: [.mediumDark]) + } else if rawValue == "👋đŸŋ" { + self.init(baseEmoji: .wave, skinTones: [.dark]) + } else if rawValue == "🤚" { + self.init(baseEmoji: .raisedBackOfHand, skinTones: nil) + } else if rawValue == "🤚đŸģ" { + self.init(baseEmoji: .raisedBackOfHand, skinTones: [.light]) + } else if rawValue == "🤚đŸŧ" { + self.init(baseEmoji: .raisedBackOfHand, skinTones: [.mediumLight]) + } else if rawValue == "🤚đŸŊ" { + self.init(baseEmoji: .raisedBackOfHand, skinTones: [.medium]) + } else if rawValue == "🤚🏾" { + self.init(baseEmoji: .raisedBackOfHand, skinTones: [.mediumDark]) + } else if rawValue == "🤚đŸŋ" { + self.init(baseEmoji: .raisedBackOfHand, skinTones: [.dark]) + } else if rawValue == "🖐ī¸" { + self.init(baseEmoji: .raisedHandWithFingersSplayed, skinTones: nil) + } else if rawValue == "🖐đŸģ" { + self.init(baseEmoji: .raisedHandWithFingersSplayed, skinTones: [.light]) + } else if rawValue == "🖐đŸŧ" { + self.init(baseEmoji: .raisedHandWithFingersSplayed, skinTones: [.mediumLight]) + } else if rawValue == "🖐đŸŊ" { + self.init(baseEmoji: .raisedHandWithFingersSplayed, skinTones: [.medium]) + } else if rawValue == "🖐🏾" { + self.init(baseEmoji: .raisedHandWithFingersSplayed, skinTones: [.mediumDark]) + } else if rawValue == "🖐đŸŋ" { + self.init(baseEmoji: .raisedHandWithFingersSplayed, skinTones: [.dark]) + } else if rawValue == "✋" { + self.init(baseEmoji: .hand, skinTones: nil) + } else if rawValue == "✋đŸģ" { + self.init(baseEmoji: .hand, skinTones: [.light]) + } else if rawValue == "✋đŸŧ" { + self.init(baseEmoji: .hand, skinTones: [.mediumLight]) + } else if rawValue == "✋đŸŊ" { + self.init(baseEmoji: .hand, skinTones: [.medium]) + } else if rawValue == "✋🏾" { + self.init(baseEmoji: .hand, skinTones: [.mediumDark]) + } else if rawValue == "✋đŸŋ" { + self.init(baseEmoji: .hand, skinTones: [.dark]) + } else if rawValue == "🖖" { + self.init(baseEmoji: .spockHand, skinTones: nil) + } else if rawValue == "🖖đŸģ" { + self.init(baseEmoji: .spockHand, skinTones: [.light]) + } else if rawValue == "🖖đŸŧ" { + self.init(baseEmoji: .spockHand, skinTones: [.mediumLight]) + } else if rawValue == "🖖đŸŊ" { + self.init(baseEmoji: .spockHand, skinTones: [.medium]) + } else if rawValue == "🖖🏾" { + self.init(baseEmoji: .spockHand, skinTones: [.mediumDark]) + } else if rawValue == "🖖đŸŋ" { + self.init(baseEmoji: .spockHand, skinTones: [.dark]) + } else if rawValue == "đŸĢą" { + self.init(baseEmoji: .rightwardsHand, skinTones: nil) + } else if rawValue == "đŸĢąđŸģ" { + self.init(baseEmoji: .rightwardsHand, skinTones: [.light]) + } else if rawValue == "đŸĢąđŸŧ" { + self.init(baseEmoji: .rightwardsHand, skinTones: [.mediumLight]) + } else if rawValue == "đŸĢąđŸŊ" { + self.init(baseEmoji: .rightwardsHand, skinTones: [.medium]) + } else if rawValue == "đŸĢąđŸž" { + self.init(baseEmoji: .rightwardsHand, skinTones: [.mediumDark]) + } else if rawValue == "đŸĢąđŸŋ" { + self.init(baseEmoji: .rightwardsHand, skinTones: [.dark]) + } else if rawValue == "đŸĢ˛" { + self.init(baseEmoji: .leftwardsHand, skinTones: nil) + } else if rawValue == "đŸĢ˛đŸģ" { + self.init(baseEmoji: .leftwardsHand, skinTones: [.light]) + } else if rawValue == "đŸĢ˛đŸŧ" { + self.init(baseEmoji: .leftwardsHand, skinTones: [.mediumLight]) + } else if rawValue == "đŸĢ˛đŸŊ" { + self.init(baseEmoji: .leftwardsHand, skinTones: [.medium]) + } else if rawValue == "đŸĢ˛đŸž" { + self.init(baseEmoji: .leftwardsHand, skinTones: [.mediumDark]) + } else if rawValue == "đŸĢ˛đŸŋ" { + self.init(baseEmoji: .leftwardsHand, skinTones: [.dark]) + } else if rawValue == "đŸĢŗ" { + self.init(baseEmoji: .palmDownHand, skinTones: nil) + } else if rawValue == "đŸĢŗđŸģ" { + self.init(baseEmoji: .palmDownHand, skinTones: [.light]) + } else if rawValue == "đŸĢŗđŸŧ" { + self.init(baseEmoji: .palmDownHand, skinTones: [.mediumLight]) + } else if rawValue == "đŸĢŗđŸŊ" { + self.init(baseEmoji: .palmDownHand, skinTones: [.medium]) + } else if rawValue == "đŸĢŗ🏾" { + self.init(baseEmoji: .palmDownHand, skinTones: [.mediumDark]) + } else if rawValue == "đŸĢŗđŸŋ" { + self.init(baseEmoji: .palmDownHand, skinTones: [.dark]) + } else if rawValue == "đŸĢ´" { + self.init(baseEmoji: .palmUpHand, skinTones: nil) + } else if rawValue == "đŸĢ´đŸģ" { + self.init(baseEmoji: .palmUpHand, skinTones: [.light]) + } else if rawValue == "đŸĢ´đŸŧ" { + self.init(baseEmoji: .palmUpHand, skinTones: [.mediumLight]) + } else if rawValue == "đŸĢ´đŸŊ" { + self.init(baseEmoji: .palmUpHand, skinTones: [.medium]) + } else if rawValue == "đŸĢ´đŸž" { + self.init(baseEmoji: .palmUpHand, skinTones: [.mediumDark]) + } else if rawValue == "đŸĢ´đŸŋ" { + self.init(baseEmoji: .palmUpHand, skinTones: [.dark]) + } else if rawValue == "👌" { + self.init(baseEmoji: .okHand, skinTones: nil) + } else if rawValue == "👌đŸģ" { + self.init(baseEmoji: .okHand, skinTones: [.light]) + } else if rawValue == "👌đŸŧ" { + self.init(baseEmoji: .okHand, skinTones: [.mediumLight]) + } else if rawValue == "👌đŸŊ" { + self.init(baseEmoji: .okHand, skinTones: [.medium]) + } else if rawValue == "👌🏾" { + self.init(baseEmoji: .okHand, skinTones: [.mediumDark]) + } else if rawValue == "👌đŸŋ" { + self.init(baseEmoji: .okHand, skinTones: [.dark]) + } else if rawValue == "🤌" { + self.init(baseEmoji: .pinchedFingers, skinTones: nil) + } else if rawValue == "🤌đŸģ" { + self.init(baseEmoji: .pinchedFingers, skinTones: [.light]) + } else if rawValue == "🤌đŸŧ" { + self.init(baseEmoji: .pinchedFingers, skinTones: [.mediumLight]) + } else if rawValue == "🤌đŸŊ" { + self.init(baseEmoji: .pinchedFingers, skinTones: [.medium]) + } else if rawValue == "🤌🏾" { + self.init(baseEmoji: .pinchedFingers, skinTones: [.mediumDark]) + } else if rawValue == "🤌đŸŋ" { + self.init(baseEmoji: .pinchedFingers, skinTones: [.dark]) + } else if rawValue == "🤏" { + self.init(baseEmoji: .pinchingHand, skinTones: nil) + } else if rawValue == "🤏đŸģ" { + self.init(baseEmoji: .pinchingHand, skinTones: [.light]) + } else if rawValue == "🤏đŸŧ" { + self.init(baseEmoji: .pinchingHand, skinTones: [.mediumLight]) + } else if rawValue == "🤏đŸŊ" { + self.init(baseEmoji: .pinchingHand, skinTones: [.medium]) + } else if rawValue == "🤏🏾" { + self.init(baseEmoji: .pinchingHand, skinTones: [.mediumDark]) + } else if rawValue == "🤏đŸŋ" { + self.init(baseEmoji: .pinchingHand, skinTones: [.dark]) + } else if rawValue == "✌ī¸" { + self.init(baseEmoji: .v, skinTones: nil) + } else if rawValue == "✌đŸģ" { + self.init(baseEmoji: .v, skinTones: [.light]) + } else if rawValue == "✌đŸŧ" { + self.init(baseEmoji: .v, skinTones: [.mediumLight]) + } else if rawValue == "✌đŸŊ" { + self.init(baseEmoji: .v, skinTones: [.medium]) + } else if rawValue == "✌🏾" { + self.init(baseEmoji: .v, skinTones: [.mediumDark]) + } else if rawValue == "✌đŸŋ" { + self.init(baseEmoji: .v, skinTones: [.dark]) + } else if rawValue == "🤞" { + self.init(baseEmoji: .crossedFingers, skinTones: nil) + } else if rawValue == "🤞đŸģ" { + self.init(baseEmoji: .crossedFingers, skinTones: [.light]) + } else if rawValue == "🤞đŸŧ" { + self.init(baseEmoji: .crossedFingers, skinTones: [.mediumLight]) + } else if rawValue == "🤞đŸŊ" { + self.init(baseEmoji: .crossedFingers, skinTones: [.medium]) + } else if rawValue == "🤞🏾" { + self.init(baseEmoji: .crossedFingers, skinTones: [.mediumDark]) + } else if rawValue == "🤞đŸŋ" { + self.init(baseEmoji: .crossedFingers, skinTones: [.dark]) + } else if rawValue == "đŸĢ°" { + self.init(baseEmoji: .handWithIndexFingerAndThumbCrossed, skinTones: nil) + } else if rawValue == "đŸĢ°đŸģ" { + self.init(baseEmoji: .handWithIndexFingerAndThumbCrossed, skinTones: [.light]) + } else if rawValue == "đŸĢ°đŸŧ" { + self.init(baseEmoji: .handWithIndexFingerAndThumbCrossed, skinTones: [.mediumLight]) + } else if rawValue == "đŸĢ°đŸŊ" { + self.init(baseEmoji: .handWithIndexFingerAndThumbCrossed, skinTones: [.medium]) + } else if rawValue == "đŸĢ°đŸž" { + self.init(baseEmoji: .handWithIndexFingerAndThumbCrossed, skinTones: [.mediumDark]) + } else if rawValue == "đŸĢ°đŸŋ" { + self.init(baseEmoji: .handWithIndexFingerAndThumbCrossed, skinTones: [.dark]) + } else if rawValue == "🤟" { + self.init(baseEmoji: .iLoveYouHandSign, skinTones: nil) + } else if rawValue == "🤟đŸģ" { + self.init(baseEmoji: .iLoveYouHandSign, skinTones: [.light]) + } else if rawValue == "🤟đŸŧ" { + self.init(baseEmoji: .iLoveYouHandSign, skinTones: [.mediumLight]) + } else if rawValue == "🤟đŸŊ" { + self.init(baseEmoji: .iLoveYouHandSign, skinTones: [.medium]) + } else if rawValue == "🤟🏾" { + self.init(baseEmoji: .iLoveYouHandSign, skinTones: [.mediumDark]) + } else if rawValue == "🤟đŸŋ" { + self.init(baseEmoji: .iLoveYouHandSign, skinTones: [.dark]) + } else if rawValue == "🤘" { + self.init(baseEmoji: .theHorns, skinTones: nil) + } else if rawValue == "🤘đŸģ" { + self.init(baseEmoji: .theHorns, skinTones: [.light]) + } else if rawValue == "🤘đŸŧ" { + self.init(baseEmoji: .theHorns, skinTones: [.mediumLight]) + } else if rawValue == "🤘đŸŊ" { + self.init(baseEmoji: .theHorns, skinTones: [.medium]) + } else if rawValue == "🤘🏾" { + self.init(baseEmoji: .theHorns, skinTones: [.mediumDark]) + } else if rawValue == "🤘đŸŋ" { + self.init(baseEmoji: .theHorns, skinTones: [.dark]) + } else if rawValue == "🤙" { + self.init(baseEmoji: .callMeHand, skinTones: nil) + } else if rawValue == "🤙đŸģ" { + self.init(baseEmoji: .callMeHand, skinTones: [.light]) + } else if rawValue == "🤙đŸŧ" { + self.init(baseEmoji: .callMeHand, skinTones: [.mediumLight]) + } else if rawValue == "🤙đŸŊ" { + self.init(baseEmoji: .callMeHand, skinTones: [.medium]) + } else if rawValue == "🤙🏾" { + self.init(baseEmoji: .callMeHand, skinTones: [.mediumDark]) + } else if rawValue == "🤙đŸŋ" { + self.init(baseEmoji: .callMeHand, skinTones: [.dark]) + } else if rawValue == "👈" { + self.init(baseEmoji: .pointLeft, skinTones: nil) + } else if rawValue == "👈đŸģ" { + self.init(baseEmoji: .pointLeft, skinTones: [.light]) + } else if rawValue == "👈đŸŧ" { + self.init(baseEmoji: .pointLeft, skinTones: [.mediumLight]) + } else if rawValue == "👈đŸŊ" { + self.init(baseEmoji: .pointLeft, skinTones: [.medium]) + } else if rawValue == "👈🏾" { + self.init(baseEmoji: .pointLeft, skinTones: [.mediumDark]) + } else if rawValue == "👈đŸŋ" { + self.init(baseEmoji: .pointLeft, skinTones: [.dark]) + } else if rawValue == "👉" { + self.init(baseEmoji: .pointRight, skinTones: nil) + } else if rawValue == "👉đŸģ" { + self.init(baseEmoji: .pointRight, skinTones: [.light]) + } else if rawValue == "👉đŸŧ" { + self.init(baseEmoji: .pointRight, skinTones: [.mediumLight]) + } else if rawValue == "👉đŸŊ" { + self.init(baseEmoji: .pointRight, skinTones: [.medium]) + } else if rawValue == "👉🏾" { + self.init(baseEmoji: .pointRight, skinTones: [.mediumDark]) + } else if rawValue == "👉đŸŋ" { + self.init(baseEmoji: .pointRight, skinTones: [.dark]) + } else if rawValue == "👆" { + self.init(baseEmoji: .pointUp2, skinTones: nil) + } else if rawValue == "👆đŸģ" { + self.init(baseEmoji: .pointUp2, skinTones: [.light]) + } else if rawValue == "👆đŸŧ" { + self.init(baseEmoji: .pointUp2, skinTones: [.mediumLight]) + } else if rawValue == "👆đŸŊ" { + self.init(baseEmoji: .pointUp2, skinTones: [.medium]) + } else if rawValue == "👆🏾" { + self.init(baseEmoji: .pointUp2, skinTones: [.mediumDark]) + } else if rawValue == "👆đŸŋ" { + self.init(baseEmoji: .pointUp2, skinTones: [.dark]) + } else if rawValue == "🖕" { + self.init(baseEmoji: .middleFinger, skinTones: nil) + } else if rawValue == "🖕đŸģ" { + self.init(baseEmoji: .middleFinger, skinTones: [.light]) + } else if rawValue == "🖕đŸŧ" { + self.init(baseEmoji: .middleFinger, skinTones: [.mediumLight]) + } else if rawValue == "🖕đŸŊ" { + self.init(baseEmoji: .middleFinger, skinTones: [.medium]) + } else if rawValue == "🖕🏾" { + self.init(baseEmoji: .middleFinger, skinTones: [.mediumDark]) + } else if rawValue == "🖕đŸŋ" { + self.init(baseEmoji: .middleFinger, skinTones: [.dark]) + } else if rawValue == "👇" { + self.init(baseEmoji: .pointDown, skinTones: nil) + } else if rawValue == "👇đŸģ" { + self.init(baseEmoji: .pointDown, skinTones: [.light]) + } else if rawValue == "👇đŸŧ" { + self.init(baseEmoji: .pointDown, skinTones: [.mediumLight]) + } else if rawValue == "👇đŸŊ" { + self.init(baseEmoji: .pointDown, skinTones: [.medium]) + } else if rawValue == "👇🏾" { + self.init(baseEmoji: .pointDown, skinTones: [.mediumDark]) + } else if rawValue == "👇đŸŋ" { + self.init(baseEmoji: .pointDown, skinTones: [.dark]) + } else if rawValue == "☝ī¸" { + self.init(baseEmoji: .pointUp, skinTones: nil) + } else if rawValue == "☝đŸģ" { + self.init(baseEmoji: .pointUp, skinTones: [.light]) + } else if rawValue == "☝đŸŧ" { + self.init(baseEmoji: .pointUp, skinTones: [.mediumLight]) + } else if rawValue == "☝đŸŊ" { + self.init(baseEmoji: .pointUp, skinTones: [.medium]) + } else if rawValue == "☝🏾" { + self.init(baseEmoji: .pointUp, skinTones: [.mediumDark]) + } else if rawValue == "☝đŸŋ" { + self.init(baseEmoji: .pointUp, skinTones: [.dark]) + } else if rawValue == "đŸĢĩ" { + self.init(baseEmoji: .indexPointingAtTheViewer, skinTones: nil) + } else if rawValue == "đŸĢĩđŸģ" { + self.init(baseEmoji: .indexPointingAtTheViewer, skinTones: [.light]) + } else if rawValue == "đŸĢĩđŸŧ" { + self.init(baseEmoji: .indexPointingAtTheViewer, skinTones: [.mediumLight]) + } else if rawValue == "đŸĢĩđŸŊ" { + self.init(baseEmoji: .indexPointingAtTheViewer, skinTones: [.medium]) + } else if rawValue == "đŸĢĩ🏾" { + self.init(baseEmoji: .indexPointingAtTheViewer, skinTones: [.mediumDark]) + } else if rawValue == "đŸĢĩđŸŋ" { + self.init(baseEmoji: .indexPointingAtTheViewer, skinTones: [.dark]) + } else if rawValue == "👍" { + self.init(baseEmoji: .plusOne, skinTones: nil) + } else if rawValue == "👍đŸģ" { + self.init(baseEmoji: .plusOne, skinTones: [.light]) + } else if rawValue == "👍đŸŧ" { + self.init(baseEmoji: .plusOne, skinTones: [.mediumLight]) + } else if rawValue == "👍đŸŊ" { + self.init(baseEmoji: .plusOne, skinTones: [.medium]) + } else if rawValue == "👍🏾" { + self.init(baseEmoji: .plusOne, skinTones: [.mediumDark]) + } else if rawValue == "👍đŸŋ" { + self.init(baseEmoji: .plusOne, skinTones: [.dark]) + } else if rawValue == "👎" { + self.init(baseEmoji: .negativeOne, skinTones: nil) + } else if rawValue == "👎đŸģ" { + self.init(baseEmoji: .negativeOne, skinTones: [.light]) + } else if rawValue == "👎đŸŧ" { + self.init(baseEmoji: .negativeOne, skinTones: [.mediumLight]) + } else if rawValue == "👎đŸŊ" { + self.init(baseEmoji: .negativeOne, skinTones: [.medium]) + } else if rawValue == "👎🏾" { + self.init(baseEmoji: .negativeOne, skinTones: [.mediumDark]) + } else if rawValue == "👎đŸŋ" { + self.init(baseEmoji: .negativeOne, skinTones: [.dark]) + } else if rawValue == "✊" { + self.init(baseEmoji: .fist, skinTones: nil) + } else if rawValue == "✊đŸģ" { + self.init(baseEmoji: .fist, skinTones: [.light]) + } else if rawValue == "✊đŸŧ" { + self.init(baseEmoji: .fist, skinTones: [.mediumLight]) + } else if rawValue == "✊đŸŊ" { + self.init(baseEmoji: .fist, skinTones: [.medium]) + } else if rawValue == "✊🏾" { + self.init(baseEmoji: .fist, skinTones: [.mediumDark]) + } else if rawValue == "✊đŸŋ" { + self.init(baseEmoji: .fist, skinTones: [.dark]) + } else if rawValue == "👊" { + self.init(baseEmoji: .facepunch, skinTones: nil) + } else if rawValue == "👊đŸģ" { + self.init(baseEmoji: .facepunch, skinTones: [.light]) + } else if rawValue == "👊đŸŧ" { + self.init(baseEmoji: .facepunch, skinTones: [.mediumLight]) + } else if rawValue == "👊đŸŊ" { + self.init(baseEmoji: .facepunch, skinTones: [.medium]) + } else if rawValue == "👊🏾" { + self.init(baseEmoji: .facepunch, skinTones: [.mediumDark]) + } else if rawValue == "👊đŸŋ" { + self.init(baseEmoji: .facepunch, skinTones: [.dark]) + } else if rawValue == "🤛" { + self.init(baseEmoji: .leftFacingFist, skinTones: nil) + } else if rawValue == "🤛đŸģ" { + self.init(baseEmoji: .leftFacingFist, skinTones: [.light]) + } else if rawValue == "🤛đŸŧ" { + self.init(baseEmoji: .leftFacingFist, skinTones: [.mediumLight]) + } else if rawValue == "🤛đŸŊ" { + self.init(baseEmoji: .leftFacingFist, skinTones: [.medium]) + } else if rawValue == "🤛🏾" { + self.init(baseEmoji: .leftFacingFist, skinTones: [.mediumDark]) + } else if rawValue == "🤛đŸŋ" { + self.init(baseEmoji: .leftFacingFist, skinTones: [.dark]) + } else if rawValue == "🤜" { + self.init(baseEmoji: .rightFacingFist, skinTones: nil) + } else if rawValue == "🤜đŸģ" { + self.init(baseEmoji: .rightFacingFist, skinTones: [.light]) + } else if rawValue == "🤜đŸŧ" { + self.init(baseEmoji: .rightFacingFist, skinTones: [.mediumLight]) + } else if rawValue == "🤜đŸŊ" { + self.init(baseEmoji: .rightFacingFist, skinTones: [.medium]) + } else if rawValue == "🤜🏾" { + self.init(baseEmoji: .rightFacingFist, skinTones: [.mediumDark]) + } else if rawValue == "🤜đŸŋ" { + self.init(baseEmoji: .rightFacingFist, skinTones: [.dark]) + } else if rawValue == "👏" { + self.init(baseEmoji: .clap, skinTones: nil) + } else if rawValue == "👏đŸģ" { + self.init(baseEmoji: .clap, skinTones: [.light]) + } else if rawValue == "👏đŸŧ" { + self.init(baseEmoji: .clap, skinTones: [.mediumLight]) + } else if rawValue == "👏đŸŊ" { + self.init(baseEmoji: .clap, skinTones: [.medium]) + } else if rawValue == "👏🏾" { + self.init(baseEmoji: .clap, skinTones: [.mediumDark]) + } else if rawValue == "👏đŸŋ" { + self.init(baseEmoji: .clap, skinTones: [.dark]) + } else if rawValue == "🙌" { + self.init(baseEmoji: .raisedHands, skinTones: nil) + } else if rawValue == "🙌đŸģ" { + self.init(baseEmoji: .raisedHands, skinTones: [.light]) + } else if rawValue == "🙌đŸŧ" { + self.init(baseEmoji: .raisedHands, skinTones: [.mediumLight]) + } else if rawValue == "🙌đŸŊ" { + self.init(baseEmoji: .raisedHands, skinTones: [.medium]) + } else if rawValue == "🙌🏾" { + self.init(baseEmoji: .raisedHands, skinTones: [.mediumDark]) + } else if rawValue == "🙌đŸŋ" { + self.init(baseEmoji: .raisedHands, skinTones: [.dark]) + } else if rawValue == "đŸĢļ" { + self.init(baseEmoji: .heartHands, skinTones: nil) + } else if rawValue == "đŸĢļđŸģ" { + self.init(baseEmoji: .heartHands, skinTones: [.light]) + } else if rawValue == "đŸĢļđŸŧ" { + self.init(baseEmoji: .heartHands, skinTones: [.mediumLight]) + } else if rawValue == "đŸĢļđŸŊ" { + self.init(baseEmoji: .heartHands, skinTones: [.medium]) + } else if rawValue == "đŸĢļ🏾" { + self.init(baseEmoji: .heartHands, skinTones: [.mediumDark]) + } else if rawValue == "đŸĢļđŸŋ" { + self.init(baseEmoji: .heartHands, skinTones: [.dark]) + } else if rawValue == "👐" { + self.init(baseEmoji: .openHands, skinTones: nil) + } else if rawValue == "👐đŸģ" { + self.init(baseEmoji: .openHands, skinTones: [.light]) + } else if rawValue == "👐đŸŧ" { + self.init(baseEmoji: .openHands, skinTones: [.mediumLight]) + } else if rawValue == "👐đŸŊ" { + self.init(baseEmoji: .openHands, skinTones: [.medium]) + } else if rawValue == "👐🏾" { + self.init(baseEmoji: .openHands, skinTones: [.mediumDark]) + } else if rawValue == "👐đŸŋ" { + self.init(baseEmoji: .openHands, skinTones: [.dark]) + } else if rawValue == "🤲" { + self.init(baseEmoji: .palmsUpTogether, skinTones: nil) + } else if rawValue == "🤲đŸģ" { + self.init(baseEmoji: .palmsUpTogether, skinTones: [.light]) + } else if rawValue == "🤲đŸŧ" { + self.init(baseEmoji: .palmsUpTogether, skinTones: [.mediumLight]) + } else if rawValue == "🤲đŸŊ" { + self.init(baseEmoji: .palmsUpTogether, skinTones: [.medium]) + } else if rawValue == "🤲🏾" { + self.init(baseEmoji: .palmsUpTogether, skinTones: [.mediumDark]) + } else if rawValue == "🤲đŸŋ" { + self.init(baseEmoji: .palmsUpTogether, skinTones: [.dark]) + } else if rawValue == "🤝" { + self.init(baseEmoji: .handshake, skinTones: nil) + } else if rawValue == "🤝đŸģ" { + self.init(baseEmoji: .handshake, skinTones: [.light]) + } else if rawValue == "đŸĢąđŸģ‍đŸĢ˛đŸŧ" { + self.init(baseEmoji: .handshake, skinTones: [.light, .mediumLight]) + } else if rawValue == "đŸĢąđŸģ‍đŸĢ˛đŸŊ" { + self.init(baseEmoji: .handshake, skinTones: [.light, .medium]) + } else if rawValue == "đŸĢąđŸģ‍đŸĢ˛đŸž" { + self.init(baseEmoji: .handshake, skinTones: [.light, .mediumDark]) + } else if rawValue == "đŸĢąđŸģ‍đŸĢ˛đŸŋ" { + self.init(baseEmoji: .handshake, skinTones: [.light, .dark]) + } else if rawValue == "🤝đŸŧ" { + self.init(baseEmoji: .handshake, skinTones: [.mediumLight]) + } else if rawValue == "đŸĢąđŸŧ‍đŸĢ˛đŸģ" { + self.init(baseEmoji: .handshake, skinTones: [.mediumLight, .light]) + } else if rawValue == "đŸĢąđŸŧ‍đŸĢ˛đŸŊ" { + self.init(baseEmoji: .handshake, skinTones: [.mediumLight, .medium]) + } else if rawValue == "đŸĢąđŸŧ‍đŸĢ˛đŸž" { + self.init(baseEmoji: .handshake, skinTones: [.mediumLight, .mediumDark]) + } else if rawValue == "đŸĢąđŸŧ‍đŸĢ˛đŸŋ" { + self.init(baseEmoji: .handshake, skinTones: [.mediumLight, .dark]) + } else if rawValue == "🤝đŸŊ" { + self.init(baseEmoji: .handshake, skinTones: [.medium]) + } else if rawValue == "đŸĢąđŸŊ‍đŸĢ˛đŸģ" { + self.init(baseEmoji: .handshake, skinTones: [.medium, .light]) + } else if rawValue == "đŸĢąđŸŊ‍đŸĢ˛đŸŧ" { + self.init(baseEmoji: .handshake, skinTones: [.medium, .mediumLight]) + } else if rawValue == "đŸĢąđŸŊ‍đŸĢ˛đŸž" { + self.init(baseEmoji: .handshake, skinTones: [.medium, .mediumDark]) + } else if rawValue == "đŸĢąđŸŊ‍đŸĢ˛đŸŋ" { + self.init(baseEmoji: .handshake, skinTones: [.medium, .dark]) + } else if rawValue == "🤝🏾" { + self.init(baseEmoji: .handshake, skinTones: [.mediumDark]) + } else if rawValue == "đŸĢąđŸžâ€đŸĢ˛đŸģ" { + self.init(baseEmoji: .handshake, skinTones: [.mediumDark, .light]) + } else if rawValue == "đŸĢąđŸžâ€đŸĢ˛đŸŧ" { + self.init(baseEmoji: .handshake, skinTones: [.mediumDark, .mediumLight]) + } else if rawValue == "đŸĢąđŸžâ€đŸĢ˛đŸŊ" { + self.init(baseEmoji: .handshake, skinTones: [.mediumDark, .medium]) + } else if rawValue == "đŸĢąđŸžâ€đŸĢ˛đŸŋ" { + self.init(baseEmoji: .handshake, skinTones: [.mediumDark, .dark]) + } else if rawValue == "🤝đŸŋ" { + self.init(baseEmoji: .handshake, skinTones: [.dark]) + } else if rawValue == "đŸĢąđŸŋ‍đŸĢ˛đŸģ" { + self.init(baseEmoji: .handshake, skinTones: [.dark, .light]) + } else if rawValue == "đŸĢąđŸŋ‍đŸĢ˛đŸŧ" { + self.init(baseEmoji: .handshake, skinTones: [.dark, .mediumLight]) + } else if rawValue == "đŸĢąđŸŋ‍đŸĢ˛đŸŊ" { + self.init(baseEmoji: .handshake, skinTones: [.dark, .medium]) + } else if rawValue == "đŸĢąđŸŋ‍đŸĢ˛đŸž" { + self.init(baseEmoji: .handshake, skinTones: [.dark, .mediumDark]) + } else if rawValue == "🙏" { + self.init(baseEmoji: .pray, skinTones: nil) + } else if rawValue == "🙏đŸģ" { + self.init(baseEmoji: .pray, skinTones: [.light]) + } else if rawValue == "🙏đŸŧ" { + self.init(baseEmoji: .pray, skinTones: [.mediumLight]) + } else if rawValue == "🙏đŸŊ" { + self.init(baseEmoji: .pray, skinTones: [.medium]) + } else if rawValue == "🙏🏾" { + self.init(baseEmoji: .pray, skinTones: [.mediumDark]) + } else if rawValue == "🙏đŸŋ" { + self.init(baseEmoji: .pray, skinTones: [.dark]) + } else if rawValue == "✍ī¸" { + self.init(baseEmoji: .writingHand, skinTones: nil) + } else if rawValue == "✍đŸģ" { + self.init(baseEmoji: .writingHand, skinTones: [.light]) + } else if rawValue == "✍đŸŧ" { + self.init(baseEmoji: .writingHand, skinTones: [.mediumLight]) + } else if rawValue == "✍đŸŊ" { + self.init(baseEmoji: .writingHand, skinTones: [.medium]) + } else if rawValue == "✍🏾" { + self.init(baseEmoji: .writingHand, skinTones: [.mediumDark]) + } else if rawValue == "✍đŸŋ" { + self.init(baseEmoji: .writingHand, skinTones: [.dark]) + } else if rawValue == "💅" { + self.init(baseEmoji: .nailCare, skinTones: nil) + } else if rawValue == "💅đŸģ" { + self.init(baseEmoji: .nailCare, skinTones: [.light]) + } else if rawValue == "💅đŸŧ" { + self.init(baseEmoji: .nailCare, skinTones: [.mediumLight]) + } else if rawValue == "💅đŸŊ" { + self.init(baseEmoji: .nailCare, skinTones: [.medium]) + } else if rawValue == "💅🏾" { + self.init(baseEmoji: .nailCare, skinTones: [.mediumDark]) + } else if rawValue == "💅đŸŋ" { + self.init(baseEmoji: .nailCare, skinTones: [.dark]) + } else if rawValue == "đŸ¤ŗ" { + self.init(baseEmoji: .selfie, skinTones: nil) + } else if rawValue == "đŸ¤ŗđŸģ" { + self.init(baseEmoji: .selfie, skinTones: [.light]) + } else if rawValue == "đŸ¤ŗđŸŧ" { + self.init(baseEmoji: .selfie, skinTones: [.mediumLight]) + } else if rawValue == "đŸ¤ŗđŸŊ" { + self.init(baseEmoji: .selfie, skinTones: [.medium]) + } else if rawValue == "đŸ¤ŗ🏾" { + self.init(baseEmoji: .selfie, skinTones: [.mediumDark]) + } else if rawValue == "đŸ¤ŗđŸŋ" { + self.init(baseEmoji: .selfie, skinTones: [.dark]) + } else if rawValue == "đŸ’Ē" { + self.init(baseEmoji: .muscle, skinTones: nil) + } else if rawValue == "đŸ’ĒđŸģ" { + self.init(baseEmoji: .muscle, skinTones: [.light]) + } else if rawValue == "đŸ’ĒđŸŧ" { + self.init(baseEmoji: .muscle, skinTones: [.mediumLight]) + } else if rawValue == "đŸ’ĒđŸŊ" { + self.init(baseEmoji: .muscle, skinTones: [.medium]) + } else if rawValue == "đŸ’Ē🏾" { + self.init(baseEmoji: .muscle, skinTones: [.mediumDark]) + } else if rawValue == "đŸ’ĒđŸŋ" { + self.init(baseEmoji: .muscle, skinTones: [.dark]) + } else if rawValue == "đŸĻž" { + self.init(baseEmoji: .mechanicalArm, skinTones: nil) + } else if rawValue == "đŸĻŋ" { + self.init(baseEmoji: .mechanicalLeg, skinTones: nil) + } else if rawValue == "đŸĻĩ" { + self.init(baseEmoji: .leg, skinTones: nil) + } else if rawValue == "đŸĻĩđŸģ" { + self.init(baseEmoji: .leg, skinTones: [.light]) + } else if rawValue == "đŸĻĩđŸŧ" { + self.init(baseEmoji: .leg, skinTones: [.mediumLight]) + } else if rawValue == "đŸĻĩđŸŊ" { + self.init(baseEmoji: .leg, skinTones: [.medium]) + } else if rawValue == "đŸĻĩ🏾" { + self.init(baseEmoji: .leg, skinTones: [.mediumDark]) + } else if rawValue == "đŸĻĩđŸŋ" { + self.init(baseEmoji: .leg, skinTones: [.dark]) + } else if rawValue == "đŸĻļ" { + self.init(baseEmoji: .foot, skinTones: nil) + } else if rawValue == "đŸĻļđŸģ" { + self.init(baseEmoji: .foot, skinTones: [.light]) + } else if rawValue == "đŸĻļđŸŧ" { + self.init(baseEmoji: .foot, skinTones: [.mediumLight]) + } else if rawValue == "đŸĻļđŸŊ" { + self.init(baseEmoji: .foot, skinTones: [.medium]) + } else if rawValue == "đŸĻļ🏾" { + self.init(baseEmoji: .foot, skinTones: [.mediumDark]) + } else if rawValue == "đŸĻļđŸŋ" { + self.init(baseEmoji: .foot, skinTones: [.dark]) + } else if rawValue == "👂" { + self.init(baseEmoji: .ear, skinTones: nil) + } else if rawValue == "👂đŸģ" { + self.init(baseEmoji: .ear, skinTones: [.light]) + } else if rawValue == "👂đŸŧ" { + self.init(baseEmoji: .ear, skinTones: [.mediumLight]) + } else if rawValue == "👂đŸŊ" { + self.init(baseEmoji: .ear, skinTones: [.medium]) + } else if rawValue == "👂🏾" { + self.init(baseEmoji: .ear, skinTones: [.mediumDark]) + } else if rawValue == "👂đŸŋ" { + self.init(baseEmoji: .ear, skinTones: [.dark]) + } else if rawValue == "đŸĻģ" { + self.init(baseEmoji: .earWithHearingAid, skinTones: nil) + } else if rawValue == "đŸĻģđŸģ" { + self.init(baseEmoji: .earWithHearingAid, skinTones: [.light]) + } else if rawValue == "đŸĻģđŸŧ" { + self.init(baseEmoji: .earWithHearingAid, skinTones: [.mediumLight]) + } else if rawValue == "đŸĻģđŸŊ" { + self.init(baseEmoji: .earWithHearingAid, skinTones: [.medium]) + } else if rawValue == "đŸĻģ🏾" { + self.init(baseEmoji: .earWithHearingAid, skinTones: [.mediumDark]) + } else if rawValue == "đŸĻģđŸŋ" { + self.init(baseEmoji: .earWithHearingAid, skinTones: [.dark]) + } else if rawValue == "👃" { + self.init(baseEmoji: .nose, skinTones: nil) + } else if rawValue == "👃đŸģ" { + self.init(baseEmoji: .nose, skinTones: [.light]) + } else if rawValue == "👃đŸŧ" { + self.init(baseEmoji: .nose, skinTones: [.mediumLight]) + } else if rawValue == "👃đŸŊ" { + self.init(baseEmoji: .nose, skinTones: [.medium]) + } else if rawValue == "👃🏾" { + self.init(baseEmoji: .nose, skinTones: [.mediumDark]) + } else if rawValue == "👃đŸŋ" { + self.init(baseEmoji: .nose, skinTones: [.dark]) + } else if rawValue == "🧠" { + self.init(baseEmoji: .brain, skinTones: nil) + } else if rawValue == "đŸĢ€" { + self.init(baseEmoji: .anatomicalHeart, skinTones: nil) + } else if rawValue == "đŸĢ" { + self.init(baseEmoji: .lungs, skinTones: nil) + } else if rawValue == "đŸĻˇ" { + self.init(baseEmoji: .tooth, skinTones: nil) + } else if rawValue == "đŸĻ´" { + self.init(baseEmoji: .bone, skinTones: nil) + } else if rawValue == "👀" { + self.init(baseEmoji: .eyes, skinTones: nil) + } else if rawValue == "👁ī¸" { + self.init(baseEmoji: .eye, skinTones: nil) + } else if rawValue == "👅" { + self.init(baseEmoji: .tongue, skinTones: nil) + } else if rawValue == "👄" { + self.init(baseEmoji: .lips, skinTones: nil) + } else if rawValue == "đŸĢĻ" { + self.init(baseEmoji: .bitingLip, skinTones: nil) + } else if rawValue == "đŸ‘ļ" { + self.init(baseEmoji: .baby, skinTones: nil) + } else if rawValue == "đŸ‘ļđŸģ" { + self.init(baseEmoji: .baby, skinTones: [.light]) + } else if rawValue == "đŸ‘ļđŸŧ" { + self.init(baseEmoji: .baby, skinTones: [.mediumLight]) + } else if rawValue == "đŸ‘ļđŸŊ" { + self.init(baseEmoji: .baby, skinTones: [.medium]) + } else if rawValue == "đŸ‘ļ🏾" { + self.init(baseEmoji: .baby, skinTones: [.mediumDark]) + } else if rawValue == "đŸ‘ļđŸŋ" { + self.init(baseEmoji: .baby, skinTones: [.dark]) + } else if rawValue == "🧒" { + self.init(baseEmoji: .child, skinTones: nil) + } else if rawValue == "🧒đŸģ" { + self.init(baseEmoji: .child, skinTones: [.light]) + } else if rawValue == "🧒đŸŧ" { + self.init(baseEmoji: .child, skinTones: [.mediumLight]) + } else if rawValue == "🧒đŸŊ" { + self.init(baseEmoji: .child, skinTones: [.medium]) + } else if rawValue == "🧒🏾" { + self.init(baseEmoji: .child, skinTones: [.mediumDark]) + } else if rawValue == "🧒đŸŋ" { + self.init(baseEmoji: .child, skinTones: [.dark]) + } else if rawValue == "đŸ‘Ļ" { + self.init(baseEmoji: .boy, skinTones: nil) + } else if rawValue == "đŸ‘ĻđŸģ" { + self.init(baseEmoji: .boy, skinTones: [.light]) + } else if rawValue == "đŸ‘ĻđŸŧ" { + self.init(baseEmoji: .boy, skinTones: [.mediumLight]) + } else if rawValue == "đŸ‘ĻđŸŊ" { + self.init(baseEmoji: .boy, skinTones: [.medium]) + } else if rawValue == "đŸ‘Ļ🏾" { + self.init(baseEmoji: .boy, skinTones: [.mediumDark]) + } else if rawValue == "đŸ‘ĻđŸŋ" { + self.init(baseEmoji: .boy, skinTones: [.dark]) + } else if rawValue == "👧" { + self.init(baseEmoji: .girl, skinTones: nil) + } else if rawValue == "👧đŸģ" { + self.init(baseEmoji: .girl, skinTones: [.light]) + } else if rawValue == "👧đŸŧ" { + self.init(baseEmoji: .girl, skinTones: [.mediumLight]) + } else if rawValue == "👧đŸŊ" { + self.init(baseEmoji: .girl, skinTones: [.medium]) + } else if rawValue == "👧🏾" { + self.init(baseEmoji: .girl, skinTones: [.mediumDark]) + } else if rawValue == "👧đŸŋ" { + self.init(baseEmoji: .girl, skinTones: [.dark]) + } else if rawValue == "🧑" { + self.init(baseEmoji: .adult, skinTones: nil) + } else if rawValue == "🧑đŸģ" { + self.init(baseEmoji: .adult, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ" { + self.init(baseEmoji: .adult, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ" { + self.init(baseEmoji: .adult, skinTones: [.medium]) + } else if rawValue == "🧑🏾" { + self.init(baseEmoji: .adult, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ" { + self.init(baseEmoji: .adult, skinTones: [.dark]) + } else if rawValue == "👱" { + self.init(baseEmoji: .personWithBlondHair, skinTones: nil) + } else if rawValue == "👱đŸģ" { + self.init(baseEmoji: .personWithBlondHair, skinTones: [.light]) + } else if rawValue == "👱đŸŧ" { + self.init(baseEmoji: .personWithBlondHair, skinTones: [.mediumLight]) + } else if rawValue == "👱đŸŊ" { + self.init(baseEmoji: .personWithBlondHair, skinTones: [.medium]) + } else if rawValue == "👱🏾" { + self.init(baseEmoji: .personWithBlondHair, skinTones: [.mediumDark]) + } else if rawValue == "👱đŸŋ" { + self.init(baseEmoji: .personWithBlondHair, skinTones: [.dark]) + } else if rawValue == "👨" { + self.init(baseEmoji: .man, skinTones: nil) + } else if rawValue == "👨đŸģ" { + self.init(baseEmoji: .man, skinTones: [.light]) + } else if rawValue == "👨đŸŧ" { + self.init(baseEmoji: .man, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ" { + self.init(baseEmoji: .man, skinTones: [.medium]) + } else if rawValue == "👨🏾" { + self.init(baseEmoji: .man, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ" { + self.init(baseEmoji: .man, skinTones: [.dark]) + } else if rawValue == "🧔" { + self.init(baseEmoji: .beardedPerson, skinTones: nil) + } else if rawValue == "🧔đŸģ" { + self.init(baseEmoji: .beardedPerson, skinTones: [.light]) + } else if rawValue == "🧔đŸŧ" { + self.init(baseEmoji: .beardedPerson, skinTones: [.mediumLight]) + } else if rawValue == "🧔đŸŊ" { + self.init(baseEmoji: .beardedPerson, skinTones: [.medium]) + } else if rawValue == "🧔🏾" { + self.init(baseEmoji: .beardedPerson, skinTones: [.mediumDark]) + } else if rawValue == "🧔đŸŋ" { + self.init(baseEmoji: .beardedPerson, skinTones: [.dark]) + } else if rawValue == "🧔‍♂ī¸" { + self.init(baseEmoji: .manWithBeard, skinTones: nil) + } else if rawValue == "🧔đŸģ‍♂ī¸" { + self.init(baseEmoji: .manWithBeard, skinTones: [.light]) + } else if rawValue == "🧔đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manWithBeard, skinTones: [.mediumLight]) + } else if rawValue == "🧔đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manWithBeard, skinTones: [.medium]) + } else if rawValue == "🧔🏾‍♂ī¸" { + self.init(baseEmoji: .manWithBeard, skinTones: [.mediumDark]) + } else if rawValue == "🧔đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manWithBeard, skinTones: [.dark]) + } else if rawValue == "🧔‍♀ī¸" { + self.init(baseEmoji: .womanWithBeard, skinTones: nil) + } else if rawValue == "🧔đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanWithBeard, skinTones: [.light]) + } else if rawValue == "🧔đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanWithBeard, skinTones: [.mediumLight]) + } else if rawValue == "🧔đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanWithBeard, skinTones: [.medium]) + } else if rawValue == "🧔🏾‍♀ī¸" { + self.init(baseEmoji: .womanWithBeard, skinTones: [.mediumDark]) + } else if rawValue == "🧔đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanWithBeard, skinTones: [.dark]) + } else if rawValue == "👨‍đŸĻ°" { + self.init(baseEmoji: .redHairedMan, skinTones: nil) + } else if rawValue == "👨đŸģ‍đŸĻ°" { + self.init(baseEmoji: .redHairedMan, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍đŸĻ°" { + self.init(baseEmoji: .redHairedMan, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍đŸĻ°" { + self.init(baseEmoji: .redHairedMan, skinTones: [.medium]) + } else if rawValue == "👨🏾‍đŸĻ°" { + self.init(baseEmoji: .redHairedMan, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍đŸĻ°" { + self.init(baseEmoji: .redHairedMan, skinTones: [.dark]) + } else if rawValue == "👨‍đŸĻą" { + self.init(baseEmoji: .curlyHairedMan, skinTones: nil) + } else if rawValue == "👨đŸģ‍đŸĻą" { + self.init(baseEmoji: .curlyHairedMan, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍đŸĻą" { + self.init(baseEmoji: .curlyHairedMan, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍đŸĻą" { + self.init(baseEmoji: .curlyHairedMan, skinTones: [.medium]) + } else if rawValue == "👨🏾‍đŸĻą" { + self.init(baseEmoji: .curlyHairedMan, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍đŸĻą" { + self.init(baseEmoji: .curlyHairedMan, skinTones: [.dark]) + } else if rawValue == "👨‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedMan, skinTones: nil) + } else if rawValue == "👨đŸģ‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedMan, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedMan, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedMan, skinTones: [.medium]) + } else if rawValue == "👨🏾‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedMan, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedMan, skinTones: [.dark]) + } else if rawValue == "👨‍đŸĻ˛" { + self.init(baseEmoji: .baldMan, skinTones: nil) + } else if rawValue == "👨đŸģ‍đŸĻ˛" { + self.init(baseEmoji: .baldMan, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍đŸĻ˛" { + self.init(baseEmoji: .baldMan, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍đŸĻ˛" { + self.init(baseEmoji: .baldMan, skinTones: [.medium]) + } else if rawValue == "👨🏾‍đŸĻ˛" { + self.init(baseEmoji: .baldMan, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍đŸĻ˛" { + self.init(baseEmoji: .baldMan, skinTones: [.dark]) + } else if rawValue == "👩" { + self.init(baseEmoji: .woman, skinTones: nil) + } else if rawValue == "👩đŸģ" { + self.init(baseEmoji: .woman, skinTones: [.light]) + } else if rawValue == "👩đŸŧ" { + self.init(baseEmoji: .woman, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ" { + self.init(baseEmoji: .woman, skinTones: [.medium]) + } else if rawValue == "👩🏾" { + self.init(baseEmoji: .woman, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ" { + self.init(baseEmoji: .woman, skinTones: [.dark]) + } else if rawValue == "👩‍đŸĻ°" { + self.init(baseEmoji: .redHairedWoman, skinTones: nil) + } else if rawValue == "👩đŸģ‍đŸĻ°" { + self.init(baseEmoji: .redHairedWoman, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍đŸĻ°" { + self.init(baseEmoji: .redHairedWoman, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍đŸĻ°" { + self.init(baseEmoji: .redHairedWoman, skinTones: [.medium]) + } else if rawValue == "👩🏾‍đŸĻ°" { + self.init(baseEmoji: .redHairedWoman, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍đŸĻ°" { + self.init(baseEmoji: .redHairedWoman, skinTones: [.dark]) + } else if rawValue == "🧑‍đŸĻ°" { + self.init(baseEmoji: .redHairedPerson, skinTones: nil) + } else if rawValue == "🧑đŸģ‍đŸĻ°" { + self.init(baseEmoji: .redHairedPerson, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍đŸĻ°" { + self.init(baseEmoji: .redHairedPerson, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍đŸĻ°" { + self.init(baseEmoji: .redHairedPerson, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍đŸĻ°" { + self.init(baseEmoji: .redHairedPerson, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍đŸĻ°" { + self.init(baseEmoji: .redHairedPerson, skinTones: [.dark]) + } else if rawValue == "👩‍đŸĻą" { + self.init(baseEmoji: .curlyHairedWoman, skinTones: nil) + } else if rawValue == "👩đŸģ‍đŸĻą" { + self.init(baseEmoji: .curlyHairedWoman, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍đŸĻą" { + self.init(baseEmoji: .curlyHairedWoman, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍đŸĻą" { + self.init(baseEmoji: .curlyHairedWoman, skinTones: [.medium]) + } else if rawValue == "👩🏾‍đŸĻą" { + self.init(baseEmoji: .curlyHairedWoman, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍đŸĻą" { + self.init(baseEmoji: .curlyHairedWoman, skinTones: [.dark]) + } else if rawValue == "🧑‍đŸĻą" { + self.init(baseEmoji: .curlyHairedPerson, skinTones: nil) + } else if rawValue == "🧑đŸģ‍đŸĻą" { + self.init(baseEmoji: .curlyHairedPerson, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍đŸĻą" { + self.init(baseEmoji: .curlyHairedPerson, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍đŸĻą" { + self.init(baseEmoji: .curlyHairedPerson, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍đŸĻą" { + self.init(baseEmoji: .curlyHairedPerson, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍đŸĻą" { + self.init(baseEmoji: .curlyHairedPerson, skinTones: [.dark]) + } else if rawValue == "👩‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedWoman, skinTones: nil) + } else if rawValue == "👩đŸģ‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedWoman, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedWoman, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedWoman, skinTones: [.medium]) + } else if rawValue == "👩🏾‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedWoman, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedWoman, skinTones: [.dark]) + } else if rawValue == "🧑‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedPerson, skinTones: nil) + } else if rawValue == "🧑đŸģ‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedPerson, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedPerson, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedPerson, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedPerson, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍đŸĻŗ" { + self.init(baseEmoji: .whiteHairedPerson, skinTones: [.dark]) + } else if rawValue == "👩‍đŸĻ˛" { + self.init(baseEmoji: .baldWoman, skinTones: nil) + } else if rawValue == "👩đŸģ‍đŸĻ˛" { + self.init(baseEmoji: .baldWoman, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍đŸĻ˛" { + self.init(baseEmoji: .baldWoman, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍đŸĻ˛" { + self.init(baseEmoji: .baldWoman, skinTones: [.medium]) + } else if rawValue == "👩🏾‍đŸĻ˛" { + self.init(baseEmoji: .baldWoman, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍đŸĻ˛" { + self.init(baseEmoji: .baldWoman, skinTones: [.dark]) + } else if rawValue == "🧑‍đŸĻ˛" { + self.init(baseEmoji: .baldPerson, skinTones: nil) + } else if rawValue == "🧑đŸģ‍đŸĻ˛" { + self.init(baseEmoji: .baldPerson, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍đŸĻ˛" { + self.init(baseEmoji: .baldPerson, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍đŸĻ˛" { + self.init(baseEmoji: .baldPerson, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍đŸĻ˛" { + self.init(baseEmoji: .baldPerson, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍đŸĻ˛" { + self.init(baseEmoji: .baldPerson, skinTones: [.dark]) + } else if rawValue == "👱‍♀ī¸" { + self.init(baseEmoji: .blondHairedWoman, skinTones: nil) + } else if rawValue == "👱đŸģ‍♀ī¸" { + self.init(baseEmoji: .blondHairedWoman, skinTones: [.light]) + } else if rawValue == "👱đŸŧ‍♀ī¸" { + self.init(baseEmoji: .blondHairedWoman, skinTones: [.mediumLight]) + } else if rawValue == "👱đŸŊ‍♀ī¸" { + self.init(baseEmoji: .blondHairedWoman, skinTones: [.medium]) + } else if rawValue == "👱🏾‍♀ī¸" { + self.init(baseEmoji: .blondHairedWoman, skinTones: [.mediumDark]) + } else if rawValue == "👱đŸŋ‍♀ī¸" { + self.init(baseEmoji: .blondHairedWoman, skinTones: [.dark]) + } else if rawValue == "👱‍♂ī¸" { + self.init(baseEmoji: .blondHairedMan, skinTones: nil) + } else if rawValue == "👱đŸģ‍♂ī¸" { + self.init(baseEmoji: .blondHairedMan, skinTones: [.light]) + } else if rawValue == "👱đŸŧ‍♂ī¸" { + self.init(baseEmoji: .blondHairedMan, skinTones: [.mediumLight]) + } else if rawValue == "👱đŸŊ‍♂ī¸" { + self.init(baseEmoji: .blondHairedMan, skinTones: [.medium]) + } else if rawValue == "👱🏾‍♂ī¸" { + self.init(baseEmoji: .blondHairedMan, skinTones: [.mediumDark]) + } else if rawValue == "👱đŸŋ‍♂ī¸" { + self.init(baseEmoji: .blondHairedMan, skinTones: [.dark]) + } else if rawValue == "🧓" { + self.init(baseEmoji: .olderAdult, skinTones: nil) + } else if rawValue == "🧓đŸģ" { + self.init(baseEmoji: .olderAdult, skinTones: [.light]) + } else if rawValue == "🧓đŸŧ" { + self.init(baseEmoji: .olderAdult, skinTones: [.mediumLight]) + } else if rawValue == "🧓đŸŊ" { + self.init(baseEmoji: .olderAdult, skinTones: [.medium]) + } else if rawValue == "🧓🏾" { + self.init(baseEmoji: .olderAdult, skinTones: [.mediumDark]) + } else if rawValue == "🧓đŸŋ" { + self.init(baseEmoji: .olderAdult, skinTones: [.dark]) + } else if rawValue == "👴" { + self.init(baseEmoji: .olderMan, skinTones: nil) + } else if rawValue == "👴đŸģ" { + self.init(baseEmoji: .olderMan, skinTones: [.light]) + } else if rawValue == "👴đŸŧ" { + self.init(baseEmoji: .olderMan, skinTones: [.mediumLight]) + } else if rawValue == "👴đŸŊ" { + self.init(baseEmoji: .olderMan, skinTones: [.medium]) + } else if rawValue == "👴🏾" { + self.init(baseEmoji: .olderMan, skinTones: [.mediumDark]) + } else if rawValue == "👴đŸŋ" { + self.init(baseEmoji: .olderMan, skinTones: [.dark]) + } else if rawValue == "đŸ‘ĩ" { + self.init(baseEmoji: .olderWoman, skinTones: nil) + } else if rawValue == "đŸ‘ĩđŸģ" { + self.init(baseEmoji: .olderWoman, skinTones: [.light]) + } else if rawValue == "đŸ‘ĩđŸŧ" { + self.init(baseEmoji: .olderWoman, skinTones: [.mediumLight]) + } else if rawValue == "đŸ‘ĩđŸŊ" { + self.init(baseEmoji: .olderWoman, skinTones: [.medium]) + } else if rawValue == "đŸ‘ĩ🏾" { + self.init(baseEmoji: .olderWoman, skinTones: [.mediumDark]) + } else if rawValue == "đŸ‘ĩđŸŋ" { + self.init(baseEmoji: .olderWoman, skinTones: [.dark]) + } else if rawValue == "🙍" { + self.init(baseEmoji: .personFrowning, skinTones: nil) + } else if rawValue == "🙍đŸģ" { + self.init(baseEmoji: .personFrowning, skinTones: [.light]) + } else if rawValue == "🙍đŸŧ" { + self.init(baseEmoji: .personFrowning, skinTones: [.mediumLight]) + } else if rawValue == "🙍đŸŊ" { + self.init(baseEmoji: .personFrowning, skinTones: [.medium]) + } else if rawValue == "🙍🏾" { + self.init(baseEmoji: .personFrowning, skinTones: [.mediumDark]) + } else if rawValue == "🙍đŸŋ" { + self.init(baseEmoji: .personFrowning, skinTones: [.dark]) + } else if rawValue == "🙍‍♂ī¸" { + self.init(baseEmoji: .manFrowning, skinTones: nil) + } else if rawValue == "🙍đŸģ‍♂ī¸" { + self.init(baseEmoji: .manFrowning, skinTones: [.light]) + } else if rawValue == "🙍đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manFrowning, skinTones: [.mediumLight]) + } else if rawValue == "🙍đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manFrowning, skinTones: [.medium]) + } else if rawValue == "🙍🏾‍♂ī¸" { + self.init(baseEmoji: .manFrowning, skinTones: [.mediumDark]) + } else if rawValue == "🙍đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manFrowning, skinTones: [.dark]) + } else if rawValue == "🙍‍♀ī¸" { + self.init(baseEmoji: .womanFrowning, skinTones: nil) + } else if rawValue == "🙍đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanFrowning, skinTones: [.light]) + } else if rawValue == "🙍đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanFrowning, skinTones: [.mediumLight]) + } else if rawValue == "🙍đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanFrowning, skinTones: [.medium]) + } else if rawValue == "🙍🏾‍♀ī¸" { + self.init(baseEmoji: .womanFrowning, skinTones: [.mediumDark]) + } else if rawValue == "🙍đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanFrowning, skinTones: [.dark]) + } else if rawValue == "🙎" { + self.init(baseEmoji: .personWithPoutingFace, skinTones: nil) + } else if rawValue == "🙎đŸģ" { + self.init(baseEmoji: .personWithPoutingFace, skinTones: [.light]) + } else if rawValue == "🙎đŸŧ" { + self.init(baseEmoji: .personWithPoutingFace, skinTones: [.mediumLight]) + } else if rawValue == "🙎đŸŊ" { + self.init(baseEmoji: .personWithPoutingFace, skinTones: [.medium]) + } else if rawValue == "🙎🏾" { + self.init(baseEmoji: .personWithPoutingFace, skinTones: [.mediumDark]) + } else if rawValue == "🙎đŸŋ" { + self.init(baseEmoji: .personWithPoutingFace, skinTones: [.dark]) + } else if rawValue == "🙎‍♂ī¸" { + self.init(baseEmoji: .manPouting, skinTones: nil) + } else if rawValue == "🙎đŸģ‍♂ī¸" { + self.init(baseEmoji: .manPouting, skinTones: [.light]) + } else if rawValue == "🙎đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manPouting, skinTones: [.mediumLight]) + } else if rawValue == "🙎đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manPouting, skinTones: [.medium]) + } else if rawValue == "🙎🏾‍♂ī¸" { + self.init(baseEmoji: .manPouting, skinTones: [.mediumDark]) + } else if rawValue == "🙎đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manPouting, skinTones: [.dark]) + } else if rawValue == "🙎‍♀ī¸" { + self.init(baseEmoji: .womanPouting, skinTones: nil) + } else if rawValue == "🙎đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanPouting, skinTones: [.light]) + } else if rawValue == "🙎đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanPouting, skinTones: [.mediumLight]) + } else if rawValue == "🙎đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanPouting, skinTones: [.medium]) + } else if rawValue == "🙎🏾‍♀ī¸" { + self.init(baseEmoji: .womanPouting, skinTones: [.mediumDark]) + } else if rawValue == "🙎đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanPouting, skinTones: [.dark]) + } else if rawValue == "🙅" { + self.init(baseEmoji: .noGood, skinTones: nil) + } else if rawValue == "🙅đŸģ" { + self.init(baseEmoji: .noGood, skinTones: [.light]) + } else if rawValue == "🙅đŸŧ" { + self.init(baseEmoji: .noGood, skinTones: [.mediumLight]) + } else if rawValue == "🙅đŸŊ" { + self.init(baseEmoji: .noGood, skinTones: [.medium]) + } else if rawValue == "🙅🏾" { + self.init(baseEmoji: .noGood, skinTones: [.mediumDark]) + } else if rawValue == "🙅đŸŋ" { + self.init(baseEmoji: .noGood, skinTones: [.dark]) + } else if rawValue == "🙅‍♂ī¸" { + self.init(baseEmoji: .manGesturingNo, skinTones: nil) + } else if rawValue == "🙅đŸģ‍♂ī¸" { + self.init(baseEmoji: .manGesturingNo, skinTones: [.light]) + } else if rawValue == "🙅đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manGesturingNo, skinTones: [.mediumLight]) + } else if rawValue == "🙅đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manGesturingNo, skinTones: [.medium]) + } else if rawValue == "🙅🏾‍♂ī¸" { + self.init(baseEmoji: .manGesturingNo, skinTones: [.mediumDark]) + } else if rawValue == "🙅đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manGesturingNo, skinTones: [.dark]) + } else if rawValue == "🙅‍♀ī¸" { + self.init(baseEmoji: .womanGesturingNo, skinTones: nil) + } else if rawValue == "🙅đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanGesturingNo, skinTones: [.light]) + } else if rawValue == "🙅đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanGesturingNo, skinTones: [.mediumLight]) + } else if rawValue == "🙅đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanGesturingNo, skinTones: [.medium]) + } else if rawValue == "🙅🏾‍♀ī¸" { + self.init(baseEmoji: .womanGesturingNo, skinTones: [.mediumDark]) + } else if rawValue == "🙅đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanGesturingNo, skinTones: [.dark]) + } else if rawValue == "🙆" { + self.init(baseEmoji: .okWoman, skinTones: nil) + } else if rawValue == "🙆đŸģ" { + self.init(baseEmoji: .okWoman, skinTones: [.light]) + } else if rawValue == "🙆đŸŧ" { + self.init(baseEmoji: .okWoman, skinTones: [.mediumLight]) + } else if rawValue == "🙆đŸŊ" { + self.init(baseEmoji: .okWoman, skinTones: [.medium]) + } else if rawValue == "🙆🏾" { + self.init(baseEmoji: .okWoman, skinTones: [.mediumDark]) + } else if rawValue == "🙆đŸŋ" { + self.init(baseEmoji: .okWoman, skinTones: [.dark]) + } else if rawValue == "🙆‍♂ī¸" { + self.init(baseEmoji: .manGesturingOk, skinTones: nil) + } else if rawValue == "🙆đŸģ‍♂ī¸" { + self.init(baseEmoji: .manGesturingOk, skinTones: [.light]) + } else if rawValue == "🙆đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manGesturingOk, skinTones: [.mediumLight]) + } else if rawValue == "🙆đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manGesturingOk, skinTones: [.medium]) + } else if rawValue == "🙆🏾‍♂ī¸" { + self.init(baseEmoji: .manGesturingOk, skinTones: [.mediumDark]) + } else if rawValue == "🙆đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manGesturingOk, skinTones: [.dark]) + } else if rawValue == "🙆‍♀ī¸" { + self.init(baseEmoji: .womanGesturingOk, skinTones: nil) + } else if rawValue == "🙆đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanGesturingOk, skinTones: [.light]) + } else if rawValue == "🙆đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanGesturingOk, skinTones: [.mediumLight]) + } else if rawValue == "🙆đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanGesturingOk, skinTones: [.medium]) + } else if rawValue == "🙆🏾‍♀ī¸" { + self.init(baseEmoji: .womanGesturingOk, skinTones: [.mediumDark]) + } else if rawValue == "🙆đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanGesturingOk, skinTones: [.dark]) + } else if rawValue == "💁" { + self.init(baseEmoji: .informationDeskPerson, skinTones: nil) + } else if rawValue == "💁đŸģ" { + self.init(baseEmoji: .informationDeskPerson, skinTones: [.light]) + } else if rawValue == "💁đŸŧ" { + self.init(baseEmoji: .informationDeskPerson, skinTones: [.mediumLight]) + } else if rawValue == "💁đŸŊ" { + self.init(baseEmoji: .informationDeskPerson, skinTones: [.medium]) + } else if rawValue == "💁🏾" { + self.init(baseEmoji: .informationDeskPerson, skinTones: [.mediumDark]) + } else if rawValue == "💁đŸŋ" { + self.init(baseEmoji: .informationDeskPerson, skinTones: [.dark]) + } else if rawValue == "💁‍♂ī¸" { + self.init(baseEmoji: .manTippingHand, skinTones: nil) + } else if rawValue == "💁đŸģ‍♂ī¸" { + self.init(baseEmoji: .manTippingHand, skinTones: [.light]) + } else if rawValue == "💁đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manTippingHand, skinTones: [.mediumLight]) + } else if rawValue == "💁đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manTippingHand, skinTones: [.medium]) + } else if rawValue == "💁🏾‍♂ī¸" { + self.init(baseEmoji: .manTippingHand, skinTones: [.mediumDark]) + } else if rawValue == "💁đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manTippingHand, skinTones: [.dark]) + } else if rawValue == "💁‍♀ī¸" { + self.init(baseEmoji: .womanTippingHand, skinTones: nil) + } else if rawValue == "💁đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanTippingHand, skinTones: [.light]) + } else if rawValue == "💁đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanTippingHand, skinTones: [.mediumLight]) + } else if rawValue == "💁đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanTippingHand, skinTones: [.medium]) + } else if rawValue == "💁🏾‍♀ī¸" { + self.init(baseEmoji: .womanTippingHand, skinTones: [.mediumDark]) + } else if rawValue == "💁đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanTippingHand, skinTones: [.dark]) + } else if rawValue == "🙋" { + self.init(baseEmoji: .raisingHand, skinTones: nil) + } else if rawValue == "🙋đŸģ" { + self.init(baseEmoji: .raisingHand, skinTones: [.light]) + } else if rawValue == "🙋đŸŧ" { + self.init(baseEmoji: .raisingHand, skinTones: [.mediumLight]) + } else if rawValue == "🙋đŸŊ" { + self.init(baseEmoji: .raisingHand, skinTones: [.medium]) + } else if rawValue == "🙋🏾" { + self.init(baseEmoji: .raisingHand, skinTones: [.mediumDark]) + } else if rawValue == "🙋đŸŋ" { + self.init(baseEmoji: .raisingHand, skinTones: [.dark]) + } else if rawValue == "🙋‍♂ī¸" { + self.init(baseEmoji: .manRaisingHand, skinTones: nil) + } else if rawValue == "🙋đŸģ‍♂ī¸" { + self.init(baseEmoji: .manRaisingHand, skinTones: [.light]) + } else if rawValue == "🙋đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manRaisingHand, skinTones: [.mediumLight]) + } else if rawValue == "🙋đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manRaisingHand, skinTones: [.medium]) + } else if rawValue == "🙋🏾‍♂ī¸" { + self.init(baseEmoji: .manRaisingHand, skinTones: [.mediumDark]) + } else if rawValue == "🙋đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manRaisingHand, skinTones: [.dark]) + } else if rawValue == "🙋‍♀ī¸" { + self.init(baseEmoji: .womanRaisingHand, skinTones: nil) + } else if rawValue == "🙋đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanRaisingHand, skinTones: [.light]) + } else if rawValue == "🙋đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanRaisingHand, skinTones: [.mediumLight]) + } else if rawValue == "🙋đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanRaisingHand, skinTones: [.medium]) + } else if rawValue == "🙋🏾‍♀ī¸" { + self.init(baseEmoji: .womanRaisingHand, skinTones: [.mediumDark]) + } else if rawValue == "🙋đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanRaisingHand, skinTones: [.dark]) + } else if rawValue == "🧏" { + self.init(baseEmoji: .deafPerson, skinTones: nil) + } else if rawValue == "🧏đŸģ" { + self.init(baseEmoji: .deafPerson, skinTones: [.light]) + } else if rawValue == "🧏đŸŧ" { + self.init(baseEmoji: .deafPerson, skinTones: [.mediumLight]) + } else if rawValue == "🧏đŸŊ" { + self.init(baseEmoji: .deafPerson, skinTones: [.medium]) + } else if rawValue == "🧏🏾" { + self.init(baseEmoji: .deafPerson, skinTones: [.mediumDark]) + } else if rawValue == "🧏đŸŋ" { + self.init(baseEmoji: .deafPerson, skinTones: [.dark]) + } else if rawValue == "🧏‍♂ī¸" { + self.init(baseEmoji: .deafMan, skinTones: nil) + } else if rawValue == "🧏đŸģ‍♂ī¸" { + self.init(baseEmoji: .deafMan, skinTones: [.light]) + } else if rawValue == "🧏đŸŧ‍♂ī¸" { + self.init(baseEmoji: .deafMan, skinTones: [.mediumLight]) + } else if rawValue == "🧏đŸŊ‍♂ī¸" { + self.init(baseEmoji: .deafMan, skinTones: [.medium]) + } else if rawValue == "🧏🏾‍♂ī¸" { + self.init(baseEmoji: .deafMan, skinTones: [.mediumDark]) + } else if rawValue == "🧏đŸŋ‍♂ī¸" { + self.init(baseEmoji: .deafMan, skinTones: [.dark]) + } else if rawValue == "🧏‍♀ī¸" { + self.init(baseEmoji: .deafWoman, skinTones: nil) + } else if rawValue == "🧏đŸģ‍♀ī¸" { + self.init(baseEmoji: .deafWoman, skinTones: [.light]) + } else if rawValue == "🧏đŸŧ‍♀ī¸" { + self.init(baseEmoji: .deafWoman, skinTones: [.mediumLight]) + } else if rawValue == "🧏đŸŊ‍♀ī¸" { + self.init(baseEmoji: .deafWoman, skinTones: [.medium]) + } else if rawValue == "🧏🏾‍♀ī¸" { + self.init(baseEmoji: .deafWoman, skinTones: [.mediumDark]) + } else if rawValue == "🧏đŸŋ‍♀ī¸" { + self.init(baseEmoji: .deafWoman, skinTones: [.dark]) + } else if rawValue == "🙇" { + self.init(baseEmoji: .bow, skinTones: nil) + } else if rawValue == "🙇đŸģ" { + self.init(baseEmoji: .bow, skinTones: [.light]) + } else if rawValue == "🙇đŸŧ" { + self.init(baseEmoji: .bow, skinTones: [.mediumLight]) + } else if rawValue == "🙇đŸŊ" { + self.init(baseEmoji: .bow, skinTones: [.medium]) + } else if rawValue == "🙇🏾" { + self.init(baseEmoji: .bow, skinTones: [.mediumDark]) + } else if rawValue == "🙇đŸŋ" { + self.init(baseEmoji: .bow, skinTones: [.dark]) + } else if rawValue == "🙇‍♂ī¸" { + self.init(baseEmoji: .manBowing, skinTones: nil) + } else if rawValue == "🙇đŸģ‍♂ī¸" { + self.init(baseEmoji: .manBowing, skinTones: [.light]) + } else if rawValue == "🙇đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manBowing, skinTones: [.mediumLight]) + } else if rawValue == "🙇đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manBowing, skinTones: [.medium]) + } else if rawValue == "🙇🏾‍♂ī¸" { + self.init(baseEmoji: .manBowing, skinTones: [.mediumDark]) + } else if rawValue == "🙇đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manBowing, skinTones: [.dark]) + } else if rawValue == "🙇‍♀ī¸" { + self.init(baseEmoji: .womanBowing, skinTones: nil) + } else if rawValue == "🙇đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanBowing, skinTones: [.light]) + } else if rawValue == "🙇đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanBowing, skinTones: [.mediumLight]) + } else if rawValue == "🙇đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanBowing, skinTones: [.medium]) + } else if rawValue == "🙇🏾‍♀ī¸" { + self.init(baseEmoji: .womanBowing, skinTones: [.mediumDark]) + } else if rawValue == "🙇đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanBowing, skinTones: [.dark]) + } else if rawValue == "đŸ¤Ļ" { + self.init(baseEmoji: .facePalm, skinTones: nil) + } else if rawValue == "đŸ¤ĻđŸģ" { + self.init(baseEmoji: .facePalm, skinTones: [.light]) + } else if rawValue == "đŸ¤ĻđŸŧ" { + self.init(baseEmoji: .facePalm, skinTones: [.mediumLight]) + } else if rawValue == "đŸ¤ĻđŸŊ" { + self.init(baseEmoji: .facePalm, skinTones: [.medium]) + } else if rawValue == "đŸ¤Ļ🏾" { + self.init(baseEmoji: .facePalm, skinTones: [.mediumDark]) + } else if rawValue == "đŸ¤ĻđŸŋ" { + self.init(baseEmoji: .facePalm, skinTones: [.dark]) + } else if rawValue == "đŸ¤Ļ‍♂ī¸" { + self.init(baseEmoji: .manFacepalming, skinTones: nil) + } else if rawValue == "đŸ¤ĻđŸģ‍♂ī¸" { + self.init(baseEmoji: .manFacepalming, skinTones: [.light]) + } else if rawValue == "đŸ¤ĻđŸŧ‍♂ī¸" { + self.init(baseEmoji: .manFacepalming, skinTones: [.mediumLight]) + } else if rawValue == "đŸ¤ĻđŸŊ‍♂ī¸" { + self.init(baseEmoji: .manFacepalming, skinTones: [.medium]) + } else if rawValue == "đŸ¤Ļ🏾‍♂ī¸" { + self.init(baseEmoji: .manFacepalming, skinTones: [.mediumDark]) + } else if rawValue == "đŸ¤ĻđŸŋ‍♂ī¸" { + self.init(baseEmoji: .manFacepalming, skinTones: [.dark]) + } else if rawValue == "đŸ¤Ļ‍♀ī¸" { + self.init(baseEmoji: .womanFacepalming, skinTones: nil) + } else if rawValue == "đŸ¤ĻđŸģ‍♀ī¸" { + self.init(baseEmoji: .womanFacepalming, skinTones: [.light]) + } else if rawValue == "đŸ¤ĻđŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanFacepalming, skinTones: [.mediumLight]) + } else if rawValue == "đŸ¤ĻđŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanFacepalming, skinTones: [.medium]) + } else if rawValue == "đŸ¤Ļ🏾‍♀ī¸" { + self.init(baseEmoji: .womanFacepalming, skinTones: [.mediumDark]) + } else if rawValue == "đŸ¤ĻđŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanFacepalming, skinTones: [.dark]) + } else if rawValue == "🤷" { + self.init(baseEmoji: .shrug, skinTones: nil) + } else if rawValue == "🤷đŸģ" { + self.init(baseEmoji: .shrug, skinTones: [.light]) + } else if rawValue == "🤷đŸŧ" { + self.init(baseEmoji: .shrug, skinTones: [.mediumLight]) + } else if rawValue == "🤷đŸŊ" { + self.init(baseEmoji: .shrug, skinTones: [.medium]) + } else if rawValue == "🤷🏾" { + self.init(baseEmoji: .shrug, skinTones: [.mediumDark]) + } else if rawValue == "🤷đŸŋ" { + self.init(baseEmoji: .shrug, skinTones: [.dark]) + } else if rawValue == "🤷‍♂ī¸" { + self.init(baseEmoji: .manShrugging, skinTones: nil) + } else if rawValue == "🤷đŸģ‍♂ī¸" { + self.init(baseEmoji: .manShrugging, skinTones: [.light]) + } else if rawValue == "🤷đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manShrugging, skinTones: [.mediumLight]) + } else if rawValue == "🤷đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manShrugging, skinTones: [.medium]) + } else if rawValue == "🤷🏾‍♂ī¸" { + self.init(baseEmoji: .manShrugging, skinTones: [.mediumDark]) + } else if rawValue == "🤷đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manShrugging, skinTones: [.dark]) + } else if rawValue == "🤷‍♀ī¸" { + self.init(baseEmoji: .womanShrugging, skinTones: nil) + } else if rawValue == "🤷đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanShrugging, skinTones: [.light]) + } else if rawValue == "🤷đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanShrugging, skinTones: [.mediumLight]) + } else if rawValue == "🤷đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanShrugging, skinTones: [.medium]) + } else if rawValue == "🤷🏾‍♀ī¸" { + self.init(baseEmoji: .womanShrugging, skinTones: [.mediumDark]) + } else if rawValue == "🤷đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanShrugging, skinTones: [.dark]) + } else if rawValue == "🧑‍⚕ī¸" { + self.init(baseEmoji: .healthWorker, skinTones: nil) + } else if rawValue == "🧑đŸģ‍⚕ī¸" { + self.init(baseEmoji: .healthWorker, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍⚕ī¸" { + self.init(baseEmoji: .healthWorker, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍⚕ī¸" { + self.init(baseEmoji: .healthWorker, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍⚕ī¸" { + self.init(baseEmoji: .healthWorker, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍⚕ī¸" { + self.init(baseEmoji: .healthWorker, skinTones: [.dark]) + } else if rawValue == "👨‍⚕ī¸" { + self.init(baseEmoji: .maleDoctor, skinTones: nil) + } else if rawValue == "👨đŸģ‍⚕ī¸" { + self.init(baseEmoji: .maleDoctor, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍⚕ī¸" { + self.init(baseEmoji: .maleDoctor, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍⚕ī¸" { + self.init(baseEmoji: .maleDoctor, skinTones: [.medium]) + } else if rawValue == "👨🏾‍⚕ī¸" { + self.init(baseEmoji: .maleDoctor, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍⚕ī¸" { + self.init(baseEmoji: .maleDoctor, skinTones: [.dark]) + } else if rawValue == "👩‍⚕ī¸" { + self.init(baseEmoji: .femaleDoctor, skinTones: nil) + } else if rawValue == "👩đŸģ‍⚕ī¸" { + self.init(baseEmoji: .femaleDoctor, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍⚕ī¸" { + self.init(baseEmoji: .femaleDoctor, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍⚕ī¸" { + self.init(baseEmoji: .femaleDoctor, skinTones: [.medium]) + } else if rawValue == "👩🏾‍⚕ī¸" { + self.init(baseEmoji: .femaleDoctor, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍⚕ī¸" { + self.init(baseEmoji: .femaleDoctor, skinTones: [.dark]) + } else if rawValue == "🧑‍🎓" { + self.init(baseEmoji: .student, skinTones: nil) + } else if rawValue == "🧑đŸģ‍🎓" { + self.init(baseEmoji: .student, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍🎓" { + self.init(baseEmoji: .student, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍🎓" { + self.init(baseEmoji: .student, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍🎓" { + self.init(baseEmoji: .student, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍🎓" { + self.init(baseEmoji: .student, skinTones: [.dark]) + } else if rawValue == "👨‍🎓" { + self.init(baseEmoji: .maleStudent, skinTones: nil) + } else if rawValue == "👨đŸģ‍🎓" { + self.init(baseEmoji: .maleStudent, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍🎓" { + self.init(baseEmoji: .maleStudent, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍🎓" { + self.init(baseEmoji: .maleStudent, skinTones: [.medium]) + } else if rawValue == "👨🏾‍🎓" { + self.init(baseEmoji: .maleStudent, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍🎓" { + self.init(baseEmoji: .maleStudent, skinTones: [.dark]) + } else if rawValue == "👩‍🎓" { + self.init(baseEmoji: .femaleStudent, skinTones: nil) + } else if rawValue == "👩đŸģ‍🎓" { + self.init(baseEmoji: .femaleStudent, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍🎓" { + self.init(baseEmoji: .femaleStudent, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍🎓" { + self.init(baseEmoji: .femaleStudent, skinTones: [.medium]) + } else if rawValue == "👩🏾‍🎓" { + self.init(baseEmoji: .femaleStudent, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍🎓" { + self.init(baseEmoji: .femaleStudent, skinTones: [.dark]) + } else if rawValue == "🧑‍đŸĢ" { + self.init(baseEmoji: .teacher, skinTones: nil) + } else if rawValue == "🧑đŸģ‍đŸĢ" { + self.init(baseEmoji: .teacher, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍đŸĢ" { + self.init(baseEmoji: .teacher, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍đŸĢ" { + self.init(baseEmoji: .teacher, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍đŸĢ" { + self.init(baseEmoji: .teacher, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍đŸĢ" { + self.init(baseEmoji: .teacher, skinTones: [.dark]) + } else if rawValue == "👨‍đŸĢ" { + self.init(baseEmoji: .maleTeacher, skinTones: nil) + } else if rawValue == "👨đŸģ‍đŸĢ" { + self.init(baseEmoji: .maleTeacher, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍đŸĢ" { + self.init(baseEmoji: .maleTeacher, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍đŸĢ" { + self.init(baseEmoji: .maleTeacher, skinTones: [.medium]) + } else if rawValue == "👨🏾‍đŸĢ" { + self.init(baseEmoji: .maleTeacher, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍đŸĢ" { + self.init(baseEmoji: .maleTeacher, skinTones: [.dark]) + } else if rawValue == "👩‍đŸĢ" { + self.init(baseEmoji: .femaleTeacher, skinTones: nil) + } else if rawValue == "👩đŸģ‍đŸĢ" { + self.init(baseEmoji: .femaleTeacher, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍đŸĢ" { + self.init(baseEmoji: .femaleTeacher, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍đŸĢ" { + self.init(baseEmoji: .femaleTeacher, skinTones: [.medium]) + } else if rawValue == "👩🏾‍đŸĢ" { + self.init(baseEmoji: .femaleTeacher, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍đŸĢ" { + self.init(baseEmoji: .femaleTeacher, skinTones: [.dark]) + } else if rawValue == "🧑‍⚖ī¸" { + self.init(baseEmoji: .judge, skinTones: nil) + } else if rawValue == "🧑đŸģ‍⚖ī¸" { + self.init(baseEmoji: .judge, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍⚖ī¸" { + self.init(baseEmoji: .judge, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍⚖ī¸" { + self.init(baseEmoji: .judge, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍⚖ī¸" { + self.init(baseEmoji: .judge, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍⚖ī¸" { + self.init(baseEmoji: .judge, skinTones: [.dark]) + } else if rawValue == "👨‍⚖ī¸" { + self.init(baseEmoji: .maleJudge, skinTones: nil) + } else if rawValue == "👨đŸģ‍⚖ī¸" { + self.init(baseEmoji: .maleJudge, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍⚖ī¸" { + self.init(baseEmoji: .maleJudge, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍⚖ī¸" { + self.init(baseEmoji: .maleJudge, skinTones: [.medium]) + } else if rawValue == "👨🏾‍⚖ī¸" { + self.init(baseEmoji: .maleJudge, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍⚖ī¸" { + self.init(baseEmoji: .maleJudge, skinTones: [.dark]) + } else if rawValue == "👩‍⚖ī¸" { + self.init(baseEmoji: .femaleJudge, skinTones: nil) + } else if rawValue == "👩đŸģ‍⚖ī¸" { + self.init(baseEmoji: .femaleJudge, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍⚖ī¸" { + self.init(baseEmoji: .femaleJudge, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍⚖ī¸" { + self.init(baseEmoji: .femaleJudge, skinTones: [.medium]) + } else if rawValue == "👩🏾‍⚖ī¸" { + self.init(baseEmoji: .femaleJudge, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍⚖ī¸" { + self.init(baseEmoji: .femaleJudge, skinTones: [.dark]) + } else if rawValue == "🧑‍🌾" { + self.init(baseEmoji: .farmer, skinTones: nil) + } else if rawValue == "🧑đŸģ‍🌾" { + self.init(baseEmoji: .farmer, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍🌾" { + self.init(baseEmoji: .farmer, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍🌾" { + self.init(baseEmoji: .farmer, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍🌾" { + self.init(baseEmoji: .farmer, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍🌾" { + self.init(baseEmoji: .farmer, skinTones: [.dark]) + } else if rawValue == "👨‍🌾" { + self.init(baseEmoji: .maleFarmer, skinTones: nil) + } else if rawValue == "👨đŸģ‍🌾" { + self.init(baseEmoji: .maleFarmer, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍🌾" { + self.init(baseEmoji: .maleFarmer, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍🌾" { + self.init(baseEmoji: .maleFarmer, skinTones: [.medium]) + } else if rawValue == "👨🏾‍🌾" { + self.init(baseEmoji: .maleFarmer, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍🌾" { + self.init(baseEmoji: .maleFarmer, skinTones: [.dark]) + } else if rawValue == "👩‍🌾" { + self.init(baseEmoji: .femaleFarmer, skinTones: nil) + } else if rawValue == "👩đŸģ‍🌾" { + self.init(baseEmoji: .femaleFarmer, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍🌾" { + self.init(baseEmoji: .femaleFarmer, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍🌾" { + self.init(baseEmoji: .femaleFarmer, skinTones: [.medium]) + } else if rawValue == "👩🏾‍🌾" { + self.init(baseEmoji: .femaleFarmer, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍🌾" { + self.init(baseEmoji: .femaleFarmer, skinTones: [.dark]) + } else if rawValue == "🧑‍đŸŗ" { + self.init(baseEmoji: .cook, skinTones: nil) + } else if rawValue == "🧑đŸģ‍đŸŗ" { + self.init(baseEmoji: .cook, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍đŸŗ" { + self.init(baseEmoji: .cook, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍đŸŗ" { + self.init(baseEmoji: .cook, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍đŸŗ" { + self.init(baseEmoji: .cook, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍đŸŗ" { + self.init(baseEmoji: .cook, skinTones: [.dark]) + } else if rawValue == "👨‍đŸŗ" { + self.init(baseEmoji: .maleCook, skinTones: nil) + } else if rawValue == "👨đŸģ‍đŸŗ" { + self.init(baseEmoji: .maleCook, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍đŸŗ" { + self.init(baseEmoji: .maleCook, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍đŸŗ" { + self.init(baseEmoji: .maleCook, skinTones: [.medium]) + } else if rawValue == "👨🏾‍đŸŗ" { + self.init(baseEmoji: .maleCook, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍đŸŗ" { + self.init(baseEmoji: .maleCook, skinTones: [.dark]) + } else if rawValue == "👩‍đŸŗ" { + self.init(baseEmoji: .femaleCook, skinTones: nil) + } else if rawValue == "👩đŸģ‍đŸŗ" { + self.init(baseEmoji: .femaleCook, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍đŸŗ" { + self.init(baseEmoji: .femaleCook, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍đŸŗ" { + self.init(baseEmoji: .femaleCook, skinTones: [.medium]) + } else if rawValue == "👩🏾‍đŸŗ" { + self.init(baseEmoji: .femaleCook, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍đŸŗ" { + self.init(baseEmoji: .femaleCook, skinTones: [.dark]) + } else if rawValue == "🧑‍🔧" { + self.init(baseEmoji: .mechanic, skinTones: nil) + } else if rawValue == "🧑đŸģ‍🔧" { + self.init(baseEmoji: .mechanic, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍🔧" { + self.init(baseEmoji: .mechanic, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍🔧" { + self.init(baseEmoji: .mechanic, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍🔧" { + self.init(baseEmoji: .mechanic, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍🔧" { + self.init(baseEmoji: .mechanic, skinTones: [.dark]) + } else if rawValue == "👨‍🔧" { + self.init(baseEmoji: .maleMechanic, skinTones: nil) + } else if rawValue == "👨đŸģ‍🔧" { + self.init(baseEmoji: .maleMechanic, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍🔧" { + self.init(baseEmoji: .maleMechanic, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍🔧" { + self.init(baseEmoji: .maleMechanic, skinTones: [.medium]) + } else if rawValue == "👨🏾‍🔧" { + self.init(baseEmoji: .maleMechanic, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍🔧" { + self.init(baseEmoji: .maleMechanic, skinTones: [.dark]) + } else if rawValue == "👩‍🔧" { + self.init(baseEmoji: .femaleMechanic, skinTones: nil) + } else if rawValue == "👩đŸģ‍🔧" { + self.init(baseEmoji: .femaleMechanic, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍🔧" { + self.init(baseEmoji: .femaleMechanic, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍🔧" { + self.init(baseEmoji: .femaleMechanic, skinTones: [.medium]) + } else if rawValue == "👩🏾‍🔧" { + self.init(baseEmoji: .femaleMechanic, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍🔧" { + self.init(baseEmoji: .femaleMechanic, skinTones: [.dark]) + } else if rawValue == "🧑‍🏭" { + self.init(baseEmoji: .factoryWorker, skinTones: nil) + } else if rawValue == "🧑đŸģ‍🏭" { + self.init(baseEmoji: .factoryWorker, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍🏭" { + self.init(baseEmoji: .factoryWorker, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍🏭" { + self.init(baseEmoji: .factoryWorker, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍🏭" { + self.init(baseEmoji: .factoryWorker, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍🏭" { + self.init(baseEmoji: .factoryWorker, skinTones: [.dark]) + } else if rawValue == "👨‍🏭" { + self.init(baseEmoji: .maleFactoryWorker, skinTones: nil) + } else if rawValue == "👨đŸģ‍🏭" { + self.init(baseEmoji: .maleFactoryWorker, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍🏭" { + self.init(baseEmoji: .maleFactoryWorker, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍🏭" { + self.init(baseEmoji: .maleFactoryWorker, skinTones: [.medium]) + } else if rawValue == "👨🏾‍🏭" { + self.init(baseEmoji: .maleFactoryWorker, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍🏭" { + self.init(baseEmoji: .maleFactoryWorker, skinTones: [.dark]) + } else if rawValue == "👩‍🏭" { + self.init(baseEmoji: .femaleFactoryWorker, skinTones: nil) + } else if rawValue == "👩đŸģ‍🏭" { + self.init(baseEmoji: .femaleFactoryWorker, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍🏭" { + self.init(baseEmoji: .femaleFactoryWorker, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍🏭" { + self.init(baseEmoji: .femaleFactoryWorker, skinTones: [.medium]) + } else if rawValue == "👩🏾‍🏭" { + self.init(baseEmoji: .femaleFactoryWorker, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍🏭" { + self.init(baseEmoji: .femaleFactoryWorker, skinTones: [.dark]) + } else if rawValue == "🧑‍đŸ’ŧ" { + self.init(baseEmoji: .officeWorker, skinTones: nil) + } else if rawValue == "🧑đŸģ‍đŸ’ŧ" { + self.init(baseEmoji: .officeWorker, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍đŸ’ŧ" { + self.init(baseEmoji: .officeWorker, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍đŸ’ŧ" { + self.init(baseEmoji: .officeWorker, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍đŸ’ŧ" { + self.init(baseEmoji: .officeWorker, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍đŸ’ŧ" { + self.init(baseEmoji: .officeWorker, skinTones: [.dark]) + } else if rawValue == "👨‍đŸ’ŧ" { + self.init(baseEmoji: .maleOfficeWorker, skinTones: nil) + } else if rawValue == "👨đŸģ‍đŸ’ŧ" { + self.init(baseEmoji: .maleOfficeWorker, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍đŸ’ŧ" { + self.init(baseEmoji: .maleOfficeWorker, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍đŸ’ŧ" { + self.init(baseEmoji: .maleOfficeWorker, skinTones: [.medium]) + } else if rawValue == "👨🏾‍đŸ’ŧ" { + self.init(baseEmoji: .maleOfficeWorker, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍đŸ’ŧ" { + self.init(baseEmoji: .maleOfficeWorker, skinTones: [.dark]) + } else if rawValue == "👩‍đŸ’ŧ" { + self.init(baseEmoji: .femaleOfficeWorker, skinTones: nil) + } else if rawValue == "👩đŸģ‍đŸ’ŧ" { + self.init(baseEmoji: .femaleOfficeWorker, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍đŸ’ŧ" { + self.init(baseEmoji: .femaleOfficeWorker, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍đŸ’ŧ" { + self.init(baseEmoji: .femaleOfficeWorker, skinTones: [.medium]) + } else if rawValue == "👩🏾‍đŸ’ŧ" { + self.init(baseEmoji: .femaleOfficeWorker, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍đŸ’ŧ" { + self.init(baseEmoji: .femaleOfficeWorker, skinTones: [.dark]) + } else if rawValue == "🧑‍đŸ”Ŧ" { + self.init(baseEmoji: .scientist, skinTones: nil) + } else if rawValue == "🧑đŸģ‍đŸ”Ŧ" { + self.init(baseEmoji: .scientist, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍đŸ”Ŧ" { + self.init(baseEmoji: .scientist, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍đŸ”Ŧ" { + self.init(baseEmoji: .scientist, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍đŸ”Ŧ" { + self.init(baseEmoji: .scientist, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍đŸ”Ŧ" { + self.init(baseEmoji: .scientist, skinTones: [.dark]) + } else if rawValue == "👨‍đŸ”Ŧ" { + self.init(baseEmoji: .maleScientist, skinTones: nil) + } else if rawValue == "👨đŸģ‍đŸ”Ŧ" { + self.init(baseEmoji: .maleScientist, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍đŸ”Ŧ" { + self.init(baseEmoji: .maleScientist, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍đŸ”Ŧ" { + self.init(baseEmoji: .maleScientist, skinTones: [.medium]) + } else if rawValue == "👨🏾‍đŸ”Ŧ" { + self.init(baseEmoji: .maleScientist, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍đŸ”Ŧ" { + self.init(baseEmoji: .maleScientist, skinTones: [.dark]) + } else if rawValue == "👩‍đŸ”Ŧ" { + self.init(baseEmoji: .femaleScientist, skinTones: nil) + } else if rawValue == "👩đŸģ‍đŸ”Ŧ" { + self.init(baseEmoji: .femaleScientist, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍đŸ”Ŧ" { + self.init(baseEmoji: .femaleScientist, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍đŸ”Ŧ" { + self.init(baseEmoji: .femaleScientist, skinTones: [.medium]) + } else if rawValue == "👩🏾‍đŸ”Ŧ" { + self.init(baseEmoji: .femaleScientist, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍đŸ”Ŧ" { + self.init(baseEmoji: .femaleScientist, skinTones: [.dark]) + } else if rawValue == "🧑‍đŸ’ģ" { + self.init(baseEmoji: .technologist, skinTones: nil) + } else if rawValue == "🧑đŸģ‍đŸ’ģ" { + self.init(baseEmoji: .technologist, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍đŸ’ģ" { + self.init(baseEmoji: .technologist, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍đŸ’ģ" { + self.init(baseEmoji: .technologist, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍đŸ’ģ" { + self.init(baseEmoji: .technologist, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍đŸ’ģ" { + self.init(baseEmoji: .technologist, skinTones: [.dark]) + } else if rawValue == "👨‍đŸ’ģ" { + self.init(baseEmoji: .maleTechnologist, skinTones: nil) + } else if rawValue == "👨đŸģ‍đŸ’ģ" { + self.init(baseEmoji: .maleTechnologist, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍đŸ’ģ" { + self.init(baseEmoji: .maleTechnologist, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍đŸ’ģ" { + self.init(baseEmoji: .maleTechnologist, skinTones: [.medium]) + } else if rawValue == "👨🏾‍đŸ’ģ" { + self.init(baseEmoji: .maleTechnologist, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍đŸ’ģ" { + self.init(baseEmoji: .maleTechnologist, skinTones: [.dark]) + } else if rawValue == "👩‍đŸ’ģ" { + self.init(baseEmoji: .femaleTechnologist, skinTones: nil) + } else if rawValue == "👩đŸģ‍đŸ’ģ" { + self.init(baseEmoji: .femaleTechnologist, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍đŸ’ģ" { + self.init(baseEmoji: .femaleTechnologist, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍đŸ’ģ" { + self.init(baseEmoji: .femaleTechnologist, skinTones: [.medium]) + } else if rawValue == "👩🏾‍đŸ’ģ" { + self.init(baseEmoji: .femaleTechnologist, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍đŸ’ģ" { + self.init(baseEmoji: .femaleTechnologist, skinTones: [.dark]) + } else if rawValue == "🧑‍🎤" { + self.init(baseEmoji: .singer, skinTones: nil) + } else if rawValue == "🧑đŸģ‍🎤" { + self.init(baseEmoji: .singer, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍🎤" { + self.init(baseEmoji: .singer, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍🎤" { + self.init(baseEmoji: .singer, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍🎤" { + self.init(baseEmoji: .singer, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍🎤" { + self.init(baseEmoji: .singer, skinTones: [.dark]) + } else if rawValue == "👨‍🎤" { + self.init(baseEmoji: .maleSinger, skinTones: nil) + } else if rawValue == "👨đŸģ‍🎤" { + self.init(baseEmoji: .maleSinger, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍🎤" { + self.init(baseEmoji: .maleSinger, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍🎤" { + self.init(baseEmoji: .maleSinger, skinTones: [.medium]) + } else if rawValue == "👨🏾‍🎤" { + self.init(baseEmoji: .maleSinger, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍🎤" { + self.init(baseEmoji: .maleSinger, skinTones: [.dark]) + } else if rawValue == "👩‍🎤" { + self.init(baseEmoji: .femaleSinger, skinTones: nil) + } else if rawValue == "👩đŸģ‍🎤" { + self.init(baseEmoji: .femaleSinger, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍🎤" { + self.init(baseEmoji: .femaleSinger, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍🎤" { + self.init(baseEmoji: .femaleSinger, skinTones: [.medium]) + } else if rawValue == "👩🏾‍🎤" { + self.init(baseEmoji: .femaleSinger, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍🎤" { + self.init(baseEmoji: .femaleSinger, skinTones: [.dark]) + } else if rawValue == "🧑‍🎨" { + self.init(baseEmoji: .artist, skinTones: nil) + } else if rawValue == "🧑đŸģ‍🎨" { + self.init(baseEmoji: .artist, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍🎨" { + self.init(baseEmoji: .artist, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍🎨" { + self.init(baseEmoji: .artist, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍🎨" { + self.init(baseEmoji: .artist, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍🎨" { + self.init(baseEmoji: .artist, skinTones: [.dark]) + } else if rawValue == "👨‍🎨" { + self.init(baseEmoji: .maleArtist, skinTones: nil) + } else if rawValue == "👨đŸģ‍🎨" { + self.init(baseEmoji: .maleArtist, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍🎨" { + self.init(baseEmoji: .maleArtist, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍🎨" { + self.init(baseEmoji: .maleArtist, skinTones: [.medium]) + } else if rawValue == "👨🏾‍🎨" { + self.init(baseEmoji: .maleArtist, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍🎨" { + self.init(baseEmoji: .maleArtist, skinTones: [.dark]) + } else if rawValue == "👩‍🎨" { + self.init(baseEmoji: .femaleArtist, skinTones: nil) + } else if rawValue == "👩đŸģ‍🎨" { + self.init(baseEmoji: .femaleArtist, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍🎨" { + self.init(baseEmoji: .femaleArtist, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍🎨" { + self.init(baseEmoji: .femaleArtist, skinTones: [.medium]) + } else if rawValue == "👩🏾‍🎨" { + self.init(baseEmoji: .femaleArtist, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍🎨" { + self.init(baseEmoji: .femaleArtist, skinTones: [.dark]) + } else if rawValue == "🧑‍✈ī¸" { + self.init(baseEmoji: .pilot, skinTones: nil) + } else if rawValue == "🧑đŸģ‍✈ī¸" { + self.init(baseEmoji: .pilot, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍✈ī¸" { + self.init(baseEmoji: .pilot, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍✈ī¸" { + self.init(baseEmoji: .pilot, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍✈ī¸" { + self.init(baseEmoji: .pilot, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍✈ī¸" { + self.init(baseEmoji: .pilot, skinTones: [.dark]) + } else if rawValue == "👨‍✈ī¸" { + self.init(baseEmoji: .malePilot, skinTones: nil) + } else if rawValue == "👨đŸģ‍✈ī¸" { + self.init(baseEmoji: .malePilot, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍✈ī¸" { + self.init(baseEmoji: .malePilot, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍✈ī¸" { + self.init(baseEmoji: .malePilot, skinTones: [.medium]) + } else if rawValue == "👨🏾‍✈ī¸" { + self.init(baseEmoji: .malePilot, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍✈ī¸" { + self.init(baseEmoji: .malePilot, skinTones: [.dark]) + } else if rawValue == "👩‍✈ī¸" { + self.init(baseEmoji: .femalePilot, skinTones: nil) + } else if rawValue == "👩đŸģ‍✈ī¸" { + self.init(baseEmoji: .femalePilot, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍✈ī¸" { + self.init(baseEmoji: .femalePilot, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍✈ī¸" { + self.init(baseEmoji: .femalePilot, skinTones: [.medium]) + } else if rawValue == "👩🏾‍✈ī¸" { + self.init(baseEmoji: .femalePilot, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍✈ī¸" { + self.init(baseEmoji: .femalePilot, skinTones: [.dark]) + } else if rawValue == "🧑‍🚀" { + self.init(baseEmoji: .astronaut, skinTones: nil) + } else if rawValue == "🧑đŸģ‍🚀" { + self.init(baseEmoji: .astronaut, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍🚀" { + self.init(baseEmoji: .astronaut, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍🚀" { + self.init(baseEmoji: .astronaut, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍🚀" { + self.init(baseEmoji: .astronaut, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍🚀" { + self.init(baseEmoji: .astronaut, skinTones: [.dark]) + } else if rawValue == "👨‍🚀" { + self.init(baseEmoji: .maleAstronaut, skinTones: nil) + } else if rawValue == "👨đŸģ‍🚀" { + self.init(baseEmoji: .maleAstronaut, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍🚀" { + self.init(baseEmoji: .maleAstronaut, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍🚀" { + self.init(baseEmoji: .maleAstronaut, skinTones: [.medium]) + } else if rawValue == "👨🏾‍🚀" { + self.init(baseEmoji: .maleAstronaut, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍🚀" { + self.init(baseEmoji: .maleAstronaut, skinTones: [.dark]) + } else if rawValue == "👩‍🚀" { + self.init(baseEmoji: .femaleAstronaut, skinTones: nil) + } else if rawValue == "👩đŸģ‍🚀" { + self.init(baseEmoji: .femaleAstronaut, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍🚀" { + self.init(baseEmoji: .femaleAstronaut, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍🚀" { + self.init(baseEmoji: .femaleAstronaut, skinTones: [.medium]) + } else if rawValue == "👩🏾‍🚀" { + self.init(baseEmoji: .femaleAstronaut, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍🚀" { + self.init(baseEmoji: .femaleAstronaut, skinTones: [.dark]) + } else if rawValue == "🧑‍🚒" { + self.init(baseEmoji: .firefighter, skinTones: nil) + } else if rawValue == "🧑đŸģ‍🚒" { + self.init(baseEmoji: .firefighter, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍🚒" { + self.init(baseEmoji: .firefighter, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍🚒" { + self.init(baseEmoji: .firefighter, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍🚒" { + self.init(baseEmoji: .firefighter, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍🚒" { + self.init(baseEmoji: .firefighter, skinTones: [.dark]) + } else if rawValue == "👨‍🚒" { + self.init(baseEmoji: .maleFirefighter, skinTones: nil) + } else if rawValue == "👨đŸģ‍🚒" { + self.init(baseEmoji: .maleFirefighter, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍🚒" { + self.init(baseEmoji: .maleFirefighter, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍🚒" { + self.init(baseEmoji: .maleFirefighter, skinTones: [.medium]) + } else if rawValue == "👨🏾‍🚒" { + self.init(baseEmoji: .maleFirefighter, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍🚒" { + self.init(baseEmoji: .maleFirefighter, skinTones: [.dark]) + } else if rawValue == "👩‍🚒" { + self.init(baseEmoji: .femaleFirefighter, skinTones: nil) + } else if rawValue == "👩đŸģ‍🚒" { + self.init(baseEmoji: .femaleFirefighter, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍🚒" { + self.init(baseEmoji: .femaleFirefighter, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍🚒" { + self.init(baseEmoji: .femaleFirefighter, skinTones: [.medium]) + } else if rawValue == "👩🏾‍🚒" { + self.init(baseEmoji: .femaleFirefighter, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍🚒" { + self.init(baseEmoji: .femaleFirefighter, skinTones: [.dark]) + } else if rawValue == "👮" { + self.init(baseEmoji: .cop, skinTones: nil) + } else if rawValue == "👮đŸģ" { + self.init(baseEmoji: .cop, skinTones: [.light]) + } else if rawValue == "👮đŸŧ" { + self.init(baseEmoji: .cop, skinTones: [.mediumLight]) + } else if rawValue == "👮đŸŊ" { + self.init(baseEmoji: .cop, skinTones: [.medium]) + } else if rawValue == "👮🏾" { + self.init(baseEmoji: .cop, skinTones: [.mediumDark]) + } else if rawValue == "👮đŸŋ" { + self.init(baseEmoji: .cop, skinTones: [.dark]) + } else if rawValue == "👮‍♂ī¸" { + self.init(baseEmoji: .malePoliceOfficer, skinTones: nil) + } else if rawValue == "👮đŸģ‍♂ī¸" { + self.init(baseEmoji: .malePoliceOfficer, skinTones: [.light]) + } else if rawValue == "👮đŸŧ‍♂ī¸" { + self.init(baseEmoji: .malePoliceOfficer, skinTones: [.mediumLight]) + } else if rawValue == "👮đŸŊ‍♂ī¸" { + self.init(baseEmoji: .malePoliceOfficer, skinTones: [.medium]) + } else if rawValue == "👮🏾‍♂ī¸" { + self.init(baseEmoji: .malePoliceOfficer, skinTones: [.mediumDark]) + } else if rawValue == "👮đŸŋ‍♂ī¸" { + self.init(baseEmoji: .malePoliceOfficer, skinTones: [.dark]) + } else if rawValue == "👮‍♀ī¸" { + self.init(baseEmoji: .femalePoliceOfficer, skinTones: nil) + } else if rawValue == "👮đŸģ‍♀ī¸" { + self.init(baseEmoji: .femalePoliceOfficer, skinTones: [.light]) + } else if rawValue == "👮đŸŧ‍♀ī¸" { + self.init(baseEmoji: .femalePoliceOfficer, skinTones: [.mediumLight]) + } else if rawValue == "👮đŸŊ‍♀ī¸" { + self.init(baseEmoji: .femalePoliceOfficer, skinTones: [.medium]) + } else if rawValue == "👮🏾‍♀ī¸" { + self.init(baseEmoji: .femalePoliceOfficer, skinTones: [.mediumDark]) + } else if rawValue == "👮đŸŋ‍♀ī¸" { + self.init(baseEmoji: .femalePoliceOfficer, skinTones: [.dark]) + } else if rawValue == "đŸ•ĩī¸" { + self.init(baseEmoji: .sleuthOrSpy, skinTones: nil) + } else if rawValue == "đŸ•ĩđŸģ" { + self.init(baseEmoji: .sleuthOrSpy, skinTones: [.light]) + } else if rawValue == "đŸ•ĩđŸŧ" { + self.init(baseEmoji: .sleuthOrSpy, skinTones: [.mediumLight]) + } else if rawValue == "đŸ•ĩđŸŊ" { + self.init(baseEmoji: .sleuthOrSpy, skinTones: [.medium]) + } else if rawValue == "đŸ•ĩ🏾" { + self.init(baseEmoji: .sleuthOrSpy, skinTones: [.mediumDark]) + } else if rawValue == "đŸ•ĩđŸŋ" { + self.init(baseEmoji: .sleuthOrSpy, skinTones: [.dark]) + } else if rawValue == "đŸ•ĩī¸â€â™‚ī¸" { + self.init(baseEmoji: .maleDetective, skinTones: nil) + } else if rawValue == "đŸ•ĩđŸģ‍♂ī¸" { + self.init(baseEmoji: .maleDetective, skinTones: [.light]) + } else if rawValue == "đŸ•ĩđŸŧ‍♂ī¸" { + self.init(baseEmoji: .maleDetective, skinTones: [.mediumLight]) + } else if rawValue == "đŸ•ĩđŸŊ‍♂ī¸" { + self.init(baseEmoji: .maleDetective, skinTones: [.medium]) + } else if rawValue == "đŸ•ĩ🏾‍♂ī¸" { + self.init(baseEmoji: .maleDetective, skinTones: [.mediumDark]) + } else if rawValue == "đŸ•ĩđŸŋ‍♂ī¸" { + self.init(baseEmoji: .maleDetective, skinTones: [.dark]) + } else if rawValue == "đŸ•ĩī¸â€â™€ī¸" { + self.init(baseEmoji: .femaleDetective, skinTones: nil) + } else if rawValue == "đŸ•ĩđŸģ‍♀ī¸" { + self.init(baseEmoji: .femaleDetective, skinTones: [.light]) + } else if rawValue == "đŸ•ĩđŸŧ‍♀ī¸" { + self.init(baseEmoji: .femaleDetective, skinTones: [.mediumLight]) + } else if rawValue == "đŸ•ĩđŸŊ‍♀ī¸" { + self.init(baseEmoji: .femaleDetective, skinTones: [.medium]) + } else if rawValue == "đŸ•ĩ🏾‍♀ī¸" { + self.init(baseEmoji: .femaleDetective, skinTones: [.mediumDark]) + } else if rawValue == "đŸ•ĩđŸŋ‍♀ī¸" { + self.init(baseEmoji: .femaleDetective, skinTones: [.dark]) + } else if rawValue == "💂" { + self.init(baseEmoji: .guardsman, skinTones: nil) + } else if rawValue == "💂đŸģ" { + self.init(baseEmoji: .guardsman, skinTones: [.light]) + } else if rawValue == "💂đŸŧ" { + self.init(baseEmoji: .guardsman, skinTones: [.mediumLight]) + } else if rawValue == "💂đŸŊ" { + self.init(baseEmoji: .guardsman, skinTones: [.medium]) + } else if rawValue == "💂🏾" { + self.init(baseEmoji: .guardsman, skinTones: [.mediumDark]) + } else if rawValue == "💂đŸŋ" { + self.init(baseEmoji: .guardsman, skinTones: [.dark]) + } else if rawValue == "💂‍♂ī¸" { + self.init(baseEmoji: .maleGuard, skinTones: nil) + } else if rawValue == "💂đŸģ‍♂ī¸" { + self.init(baseEmoji: .maleGuard, skinTones: [.light]) + } else if rawValue == "💂đŸŧ‍♂ī¸" { + self.init(baseEmoji: .maleGuard, skinTones: [.mediumLight]) + } else if rawValue == "💂đŸŊ‍♂ī¸" { + self.init(baseEmoji: .maleGuard, skinTones: [.medium]) + } else if rawValue == "💂🏾‍♂ī¸" { + self.init(baseEmoji: .maleGuard, skinTones: [.mediumDark]) + } else if rawValue == "💂đŸŋ‍♂ī¸" { + self.init(baseEmoji: .maleGuard, skinTones: [.dark]) + } else if rawValue == "💂‍♀ī¸" { + self.init(baseEmoji: .femaleGuard, skinTones: nil) + } else if rawValue == "💂đŸģ‍♀ī¸" { + self.init(baseEmoji: .femaleGuard, skinTones: [.light]) + } else if rawValue == "💂đŸŧ‍♀ī¸" { + self.init(baseEmoji: .femaleGuard, skinTones: [.mediumLight]) + } else if rawValue == "💂đŸŊ‍♀ī¸" { + self.init(baseEmoji: .femaleGuard, skinTones: [.medium]) + } else if rawValue == "💂🏾‍♀ī¸" { + self.init(baseEmoji: .femaleGuard, skinTones: [.mediumDark]) + } else if rawValue == "💂đŸŋ‍♀ī¸" { + self.init(baseEmoji: .femaleGuard, skinTones: [.dark]) + } else if rawValue == "đŸĨˇ" { + self.init(baseEmoji: .ninja, skinTones: nil) + } else if rawValue == "đŸĨˇđŸģ" { + self.init(baseEmoji: .ninja, skinTones: [.light]) + } else if rawValue == "đŸĨˇđŸŧ" { + self.init(baseEmoji: .ninja, skinTones: [.mediumLight]) + } else if rawValue == "đŸĨˇđŸŊ" { + self.init(baseEmoji: .ninja, skinTones: [.medium]) + } else if rawValue == "đŸĨˇđŸž" { + self.init(baseEmoji: .ninja, skinTones: [.mediumDark]) + } else if rawValue == "đŸĨˇđŸŋ" { + self.init(baseEmoji: .ninja, skinTones: [.dark]) + } else if rawValue == "👷" { + self.init(baseEmoji: .constructionWorker, skinTones: nil) + } else if rawValue == "👷đŸģ" { + self.init(baseEmoji: .constructionWorker, skinTones: [.light]) + } else if rawValue == "👷đŸŧ" { + self.init(baseEmoji: .constructionWorker, skinTones: [.mediumLight]) + } else if rawValue == "👷đŸŊ" { + self.init(baseEmoji: .constructionWorker, skinTones: [.medium]) + } else if rawValue == "👷🏾" { + self.init(baseEmoji: .constructionWorker, skinTones: [.mediumDark]) + } else if rawValue == "👷đŸŋ" { + self.init(baseEmoji: .constructionWorker, skinTones: [.dark]) + } else if rawValue == "👷‍♂ī¸" { + self.init(baseEmoji: .maleConstructionWorker, skinTones: nil) + } else if rawValue == "👷đŸģ‍♂ī¸" { + self.init(baseEmoji: .maleConstructionWorker, skinTones: [.light]) + } else if rawValue == "👷đŸŧ‍♂ī¸" { + self.init(baseEmoji: .maleConstructionWorker, skinTones: [.mediumLight]) + } else if rawValue == "👷đŸŊ‍♂ī¸" { + self.init(baseEmoji: .maleConstructionWorker, skinTones: [.medium]) + } else if rawValue == "👷🏾‍♂ī¸" { + self.init(baseEmoji: .maleConstructionWorker, skinTones: [.mediumDark]) + } else if rawValue == "👷đŸŋ‍♂ī¸" { + self.init(baseEmoji: .maleConstructionWorker, skinTones: [.dark]) + } else if rawValue == "👷‍♀ī¸" { + self.init(baseEmoji: .femaleConstructionWorker, skinTones: nil) + } else if rawValue == "👷đŸģ‍♀ī¸" { + self.init(baseEmoji: .femaleConstructionWorker, skinTones: [.light]) + } else if rawValue == "👷đŸŧ‍♀ī¸" { + self.init(baseEmoji: .femaleConstructionWorker, skinTones: [.mediumLight]) + } else if rawValue == "👷đŸŊ‍♀ī¸" { + self.init(baseEmoji: .femaleConstructionWorker, skinTones: [.medium]) + } else if rawValue == "👷🏾‍♀ī¸" { + self.init(baseEmoji: .femaleConstructionWorker, skinTones: [.mediumDark]) + } else if rawValue == "👷đŸŋ‍♀ī¸" { + self.init(baseEmoji: .femaleConstructionWorker, skinTones: [.dark]) + } else if rawValue == "đŸĢ…" { + self.init(baseEmoji: .personWithCrown, skinTones: nil) + } else if rawValue == "đŸĢ…đŸģ" { + self.init(baseEmoji: .personWithCrown, skinTones: [.light]) + } else if rawValue == "đŸĢ…đŸŧ" { + self.init(baseEmoji: .personWithCrown, skinTones: [.mediumLight]) + } else if rawValue == "đŸĢ…đŸŊ" { + self.init(baseEmoji: .personWithCrown, skinTones: [.medium]) + } else if rawValue == "đŸĢ…đŸž" { + self.init(baseEmoji: .personWithCrown, skinTones: [.mediumDark]) + } else if rawValue == "đŸĢ…đŸŋ" { + self.init(baseEmoji: .personWithCrown, skinTones: [.dark]) + } else if rawValue == "🤴" { + self.init(baseEmoji: .prince, skinTones: nil) + } else if rawValue == "🤴đŸģ" { + self.init(baseEmoji: .prince, skinTones: [.light]) + } else if rawValue == "🤴đŸŧ" { + self.init(baseEmoji: .prince, skinTones: [.mediumLight]) + } else if rawValue == "🤴đŸŊ" { + self.init(baseEmoji: .prince, skinTones: [.medium]) + } else if rawValue == "🤴🏾" { + self.init(baseEmoji: .prince, skinTones: [.mediumDark]) + } else if rawValue == "🤴đŸŋ" { + self.init(baseEmoji: .prince, skinTones: [.dark]) + } else if rawValue == "👸" { + self.init(baseEmoji: .princess, skinTones: nil) + } else if rawValue == "👸đŸģ" { + self.init(baseEmoji: .princess, skinTones: [.light]) + } else if rawValue == "👸đŸŧ" { + self.init(baseEmoji: .princess, skinTones: [.mediumLight]) + } else if rawValue == "👸đŸŊ" { + self.init(baseEmoji: .princess, skinTones: [.medium]) + } else if rawValue == "👸🏾" { + self.init(baseEmoji: .princess, skinTones: [.mediumDark]) + } else if rawValue == "👸đŸŋ" { + self.init(baseEmoji: .princess, skinTones: [.dark]) + } else if rawValue == "đŸ‘ŗ" { + self.init(baseEmoji: .manWithTurban, skinTones: nil) + } else if rawValue == "đŸ‘ŗđŸģ" { + self.init(baseEmoji: .manWithTurban, skinTones: [.light]) + } else if rawValue == "đŸ‘ŗđŸŧ" { + self.init(baseEmoji: .manWithTurban, skinTones: [.mediumLight]) + } else if rawValue == "đŸ‘ŗđŸŊ" { + self.init(baseEmoji: .manWithTurban, skinTones: [.medium]) + } else if rawValue == "đŸ‘ŗ🏾" { + self.init(baseEmoji: .manWithTurban, skinTones: [.mediumDark]) + } else if rawValue == "đŸ‘ŗđŸŋ" { + self.init(baseEmoji: .manWithTurban, skinTones: [.dark]) + } else if rawValue == "đŸ‘ŗ‍♂ī¸" { + self.init(baseEmoji: .manWearingTurban, skinTones: nil) + } else if rawValue == "đŸ‘ŗđŸģ‍♂ī¸" { + self.init(baseEmoji: .manWearingTurban, skinTones: [.light]) + } else if rawValue == "đŸ‘ŗđŸŧ‍♂ī¸" { + self.init(baseEmoji: .manWearingTurban, skinTones: [.mediumLight]) + } else if rawValue == "đŸ‘ŗđŸŊ‍♂ī¸" { + self.init(baseEmoji: .manWearingTurban, skinTones: [.medium]) + } else if rawValue == "đŸ‘ŗ🏾‍♂ī¸" { + self.init(baseEmoji: .manWearingTurban, skinTones: [.mediumDark]) + } else if rawValue == "đŸ‘ŗđŸŋ‍♂ī¸" { + self.init(baseEmoji: .manWearingTurban, skinTones: [.dark]) + } else if rawValue == "đŸ‘ŗ‍♀ī¸" { + self.init(baseEmoji: .womanWearingTurban, skinTones: nil) + } else if rawValue == "đŸ‘ŗđŸģ‍♀ī¸" { + self.init(baseEmoji: .womanWearingTurban, skinTones: [.light]) + } else if rawValue == "đŸ‘ŗđŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanWearingTurban, skinTones: [.mediumLight]) + } else if rawValue == "đŸ‘ŗđŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanWearingTurban, skinTones: [.medium]) + } else if rawValue == "đŸ‘ŗ🏾‍♀ī¸" { + self.init(baseEmoji: .womanWearingTurban, skinTones: [.mediumDark]) + } else if rawValue == "đŸ‘ŗđŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanWearingTurban, skinTones: [.dark]) + } else if rawValue == "👲" { + self.init(baseEmoji: .manWithGuaPiMao, skinTones: nil) + } else if rawValue == "👲đŸģ" { + self.init(baseEmoji: .manWithGuaPiMao, skinTones: [.light]) + } else if rawValue == "👲đŸŧ" { + self.init(baseEmoji: .manWithGuaPiMao, skinTones: [.mediumLight]) + } else if rawValue == "👲đŸŊ" { + self.init(baseEmoji: .manWithGuaPiMao, skinTones: [.medium]) + } else if rawValue == "👲🏾" { + self.init(baseEmoji: .manWithGuaPiMao, skinTones: [.mediumDark]) + } else if rawValue == "👲đŸŋ" { + self.init(baseEmoji: .manWithGuaPiMao, skinTones: [.dark]) + } else if rawValue == "🧕" { + self.init(baseEmoji: .personWithHeadscarf, skinTones: nil) + } else if rawValue == "🧕đŸģ" { + self.init(baseEmoji: .personWithHeadscarf, skinTones: [.light]) + } else if rawValue == "🧕đŸŧ" { + self.init(baseEmoji: .personWithHeadscarf, skinTones: [.mediumLight]) + } else if rawValue == "🧕đŸŊ" { + self.init(baseEmoji: .personWithHeadscarf, skinTones: [.medium]) + } else if rawValue == "🧕🏾" { + self.init(baseEmoji: .personWithHeadscarf, skinTones: [.mediumDark]) + } else if rawValue == "🧕đŸŋ" { + self.init(baseEmoji: .personWithHeadscarf, skinTones: [.dark]) + } else if rawValue == "đŸ¤ĩ" { + self.init(baseEmoji: .personInTuxedo, skinTones: nil) + } else if rawValue == "đŸ¤ĩđŸģ" { + self.init(baseEmoji: .personInTuxedo, skinTones: [.light]) + } else if rawValue == "đŸ¤ĩđŸŧ" { + self.init(baseEmoji: .personInTuxedo, skinTones: [.mediumLight]) + } else if rawValue == "đŸ¤ĩđŸŊ" { + self.init(baseEmoji: .personInTuxedo, skinTones: [.medium]) + } else if rawValue == "đŸ¤ĩ🏾" { + self.init(baseEmoji: .personInTuxedo, skinTones: [.mediumDark]) + } else if rawValue == "đŸ¤ĩđŸŋ" { + self.init(baseEmoji: .personInTuxedo, skinTones: [.dark]) + } else if rawValue == "đŸ¤ĩ‍♂ī¸" { + self.init(baseEmoji: .manInTuxedo, skinTones: nil) + } else if rawValue == "đŸ¤ĩđŸģ‍♂ī¸" { + self.init(baseEmoji: .manInTuxedo, skinTones: [.light]) + } else if rawValue == "đŸ¤ĩđŸŧ‍♂ī¸" { + self.init(baseEmoji: .manInTuxedo, skinTones: [.mediumLight]) + } else if rawValue == "đŸ¤ĩđŸŊ‍♂ī¸" { + self.init(baseEmoji: .manInTuxedo, skinTones: [.medium]) + } else if rawValue == "đŸ¤ĩ🏾‍♂ī¸" { + self.init(baseEmoji: .manInTuxedo, skinTones: [.mediumDark]) + } else if rawValue == "đŸ¤ĩđŸŋ‍♂ī¸" { + self.init(baseEmoji: .manInTuxedo, skinTones: [.dark]) + } else if rawValue == "đŸ¤ĩ‍♀ī¸" { + self.init(baseEmoji: .womanInTuxedo, skinTones: nil) + } else if rawValue == "đŸ¤ĩđŸģ‍♀ī¸" { + self.init(baseEmoji: .womanInTuxedo, skinTones: [.light]) + } else if rawValue == "đŸ¤ĩđŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanInTuxedo, skinTones: [.mediumLight]) + } else if rawValue == "đŸ¤ĩđŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanInTuxedo, skinTones: [.medium]) + } else if rawValue == "đŸ¤ĩ🏾‍♀ī¸" { + self.init(baseEmoji: .womanInTuxedo, skinTones: [.mediumDark]) + } else if rawValue == "đŸ¤ĩđŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanInTuxedo, skinTones: [.dark]) + } else if rawValue == "👰" { + self.init(baseEmoji: .brideWithVeil, skinTones: nil) + } else if rawValue == "👰đŸģ" { + self.init(baseEmoji: .brideWithVeil, skinTones: [.light]) + } else if rawValue == "👰đŸŧ" { + self.init(baseEmoji: .brideWithVeil, skinTones: [.mediumLight]) + } else if rawValue == "👰đŸŊ" { + self.init(baseEmoji: .brideWithVeil, skinTones: [.medium]) + } else if rawValue == "👰🏾" { + self.init(baseEmoji: .brideWithVeil, skinTones: [.mediumDark]) + } else if rawValue == "👰đŸŋ" { + self.init(baseEmoji: .brideWithVeil, skinTones: [.dark]) + } else if rawValue == "👰‍♂ī¸" { + self.init(baseEmoji: .manWithVeil, skinTones: nil) + } else if rawValue == "👰đŸģ‍♂ī¸" { + self.init(baseEmoji: .manWithVeil, skinTones: [.light]) + } else if rawValue == "👰đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manWithVeil, skinTones: [.mediumLight]) + } else if rawValue == "👰đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manWithVeil, skinTones: [.medium]) + } else if rawValue == "👰🏾‍♂ī¸" { + self.init(baseEmoji: .manWithVeil, skinTones: [.mediumDark]) + } else if rawValue == "👰đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manWithVeil, skinTones: [.dark]) + } else if rawValue == "👰‍♀ī¸" { + self.init(baseEmoji: .womanWithVeil, skinTones: nil) + } else if rawValue == "👰đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanWithVeil, skinTones: [.light]) + } else if rawValue == "👰đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanWithVeil, skinTones: [.mediumLight]) + } else if rawValue == "👰đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanWithVeil, skinTones: [.medium]) + } else if rawValue == "👰🏾‍♀ī¸" { + self.init(baseEmoji: .womanWithVeil, skinTones: [.mediumDark]) + } else if rawValue == "👰đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanWithVeil, skinTones: [.dark]) + } else if rawValue == "🤰" { + self.init(baseEmoji: .pregnantWoman, skinTones: nil) + } else if rawValue == "🤰đŸģ" { + self.init(baseEmoji: .pregnantWoman, skinTones: [.light]) + } else if rawValue == "🤰đŸŧ" { + self.init(baseEmoji: .pregnantWoman, skinTones: [.mediumLight]) + } else if rawValue == "🤰đŸŊ" { + self.init(baseEmoji: .pregnantWoman, skinTones: [.medium]) + } else if rawValue == "🤰🏾" { + self.init(baseEmoji: .pregnantWoman, skinTones: [.mediumDark]) + } else if rawValue == "🤰đŸŋ" { + self.init(baseEmoji: .pregnantWoman, skinTones: [.dark]) + } else if rawValue == "đŸĢƒ" { + self.init(baseEmoji: .pregnantMan, skinTones: nil) + } else if rawValue == "đŸĢƒđŸģ" { + self.init(baseEmoji: .pregnantMan, skinTones: [.light]) + } else if rawValue == "đŸĢƒđŸŧ" { + self.init(baseEmoji: .pregnantMan, skinTones: [.mediumLight]) + } else if rawValue == "đŸĢƒđŸŊ" { + self.init(baseEmoji: .pregnantMan, skinTones: [.medium]) + } else if rawValue == "đŸĢƒđŸž" { + self.init(baseEmoji: .pregnantMan, skinTones: [.mediumDark]) + } else if rawValue == "đŸĢƒđŸŋ" { + self.init(baseEmoji: .pregnantMan, skinTones: [.dark]) + } else if rawValue == "đŸĢ„" { + self.init(baseEmoji: .pregnantPerson, skinTones: nil) + } else if rawValue == "đŸĢ„đŸģ" { + self.init(baseEmoji: .pregnantPerson, skinTones: [.light]) + } else if rawValue == "đŸĢ„đŸŧ" { + self.init(baseEmoji: .pregnantPerson, skinTones: [.mediumLight]) + } else if rawValue == "đŸĢ„đŸŊ" { + self.init(baseEmoji: .pregnantPerson, skinTones: [.medium]) + } else if rawValue == "đŸĢ„đŸž" { + self.init(baseEmoji: .pregnantPerson, skinTones: [.mediumDark]) + } else if rawValue == "đŸĢ„đŸŋ" { + self.init(baseEmoji: .pregnantPerson, skinTones: [.dark]) + } else if rawValue == "🤱" { + self.init(baseEmoji: .breastFeeding, skinTones: nil) + } else if rawValue == "🤱đŸģ" { + self.init(baseEmoji: .breastFeeding, skinTones: [.light]) + } else if rawValue == "🤱đŸŧ" { + self.init(baseEmoji: .breastFeeding, skinTones: [.mediumLight]) + } else if rawValue == "🤱đŸŊ" { + self.init(baseEmoji: .breastFeeding, skinTones: [.medium]) + } else if rawValue == "🤱🏾" { + self.init(baseEmoji: .breastFeeding, skinTones: [.mediumDark]) + } else if rawValue == "🤱đŸŋ" { + self.init(baseEmoji: .breastFeeding, skinTones: [.dark]) + } else if rawValue == "👩‍đŸŧ" { + self.init(baseEmoji: .womanFeedingBaby, skinTones: nil) + } else if rawValue == "👩đŸģ‍đŸŧ" { + self.init(baseEmoji: .womanFeedingBaby, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍đŸŧ" { + self.init(baseEmoji: .womanFeedingBaby, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍đŸŧ" { + self.init(baseEmoji: .womanFeedingBaby, skinTones: [.medium]) + } else if rawValue == "👩🏾‍đŸŧ" { + self.init(baseEmoji: .womanFeedingBaby, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍đŸŧ" { + self.init(baseEmoji: .womanFeedingBaby, skinTones: [.dark]) + } else if rawValue == "👨‍đŸŧ" { + self.init(baseEmoji: .manFeedingBaby, skinTones: nil) + } else if rawValue == "👨đŸģ‍đŸŧ" { + self.init(baseEmoji: .manFeedingBaby, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍đŸŧ" { + self.init(baseEmoji: .manFeedingBaby, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍đŸŧ" { + self.init(baseEmoji: .manFeedingBaby, skinTones: [.medium]) + } else if rawValue == "👨🏾‍đŸŧ" { + self.init(baseEmoji: .manFeedingBaby, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍đŸŧ" { + self.init(baseEmoji: .manFeedingBaby, skinTones: [.dark]) + } else if rawValue == "🧑‍đŸŧ" { + self.init(baseEmoji: .personFeedingBaby, skinTones: nil) + } else if rawValue == "🧑đŸģ‍đŸŧ" { + self.init(baseEmoji: .personFeedingBaby, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍đŸŧ" { + self.init(baseEmoji: .personFeedingBaby, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍đŸŧ" { + self.init(baseEmoji: .personFeedingBaby, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍đŸŧ" { + self.init(baseEmoji: .personFeedingBaby, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍đŸŧ" { + self.init(baseEmoji: .personFeedingBaby, skinTones: [.dark]) + } else if rawValue == "đŸ‘ŧ" { + self.init(baseEmoji: .angel, skinTones: nil) + } else if rawValue == "đŸ‘ŧđŸģ" { + self.init(baseEmoji: .angel, skinTones: [.light]) + } else if rawValue == "đŸ‘ŧđŸŧ" { + self.init(baseEmoji: .angel, skinTones: [.mediumLight]) + } else if rawValue == "đŸ‘ŧđŸŊ" { + self.init(baseEmoji: .angel, skinTones: [.medium]) + } else if rawValue == "đŸ‘ŧ🏾" { + self.init(baseEmoji: .angel, skinTones: [.mediumDark]) + } else if rawValue == "đŸ‘ŧđŸŋ" { + self.init(baseEmoji: .angel, skinTones: [.dark]) + } else if rawValue == "🎅" { + self.init(baseEmoji: .santa, skinTones: nil) + } else if rawValue == "🎅đŸģ" { + self.init(baseEmoji: .santa, skinTones: [.light]) + } else if rawValue == "🎅đŸŧ" { + self.init(baseEmoji: .santa, skinTones: [.mediumLight]) + } else if rawValue == "🎅đŸŊ" { + self.init(baseEmoji: .santa, skinTones: [.medium]) + } else if rawValue == "🎅🏾" { + self.init(baseEmoji: .santa, skinTones: [.mediumDark]) + } else if rawValue == "🎅đŸŋ" { + self.init(baseEmoji: .santa, skinTones: [.dark]) + } else if rawValue == "đŸ¤ļ" { + self.init(baseEmoji: .mrsClaus, skinTones: nil) + } else if rawValue == "đŸ¤ļđŸģ" { + self.init(baseEmoji: .mrsClaus, skinTones: [.light]) + } else if rawValue == "đŸ¤ļđŸŧ" { + self.init(baseEmoji: .mrsClaus, skinTones: [.mediumLight]) + } else if rawValue == "đŸ¤ļđŸŊ" { + self.init(baseEmoji: .mrsClaus, skinTones: [.medium]) + } else if rawValue == "đŸ¤ļ🏾" { + self.init(baseEmoji: .mrsClaus, skinTones: [.mediumDark]) + } else if rawValue == "đŸ¤ļđŸŋ" { + self.init(baseEmoji: .mrsClaus, skinTones: [.dark]) + } else if rawValue == "🧑‍🎄" { + self.init(baseEmoji: .mxClaus, skinTones: nil) + } else if rawValue == "🧑đŸģ‍🎄" { + self.init(baseEmoji: .mxClaus, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍🎄" { + self.init(baseEmoji: .mxClaus, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍🎄" { + self.init(baseEmoji: .mxClaus, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍🎄" { + self.init(baseEmoji: .mxClaus, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍🎄" { + self.init(baseEmoji: .mxClaus, skinTones: [.dark]) + } else if rawValue == "đŸĻ¸" { + self.init(baseEmoji: .superhero, skinTones: nil) + } else if rawValue == "đŸĻ¸đŸģ" { + self.init(baseEmoji: .superhero, skinTones: [.light]) + } else if rawValue == "đŸĻ¸đŸŧ" { + self.init(baseEmoji: .superhero, skinTones: [.mediumLight]) + } else if rawValue == "đŸĻ¸đŸŊ" { + self.init(baseEmoji: .superhero, skinTones: [.medium]) + } else if rawValue == "đŸĻ¸đŸž" { + self.init(baseEmoji: .superhero, skinTones: [.mediumDark]) + } else if rawValue == "đŸĻ¸đŸŋ" { + self.init(baseEmoji: .superhero, skinTones: [.dark]) + } else if rawValue == "đŸĻ¸â€â™‚ī¸" { + self.init(baseEmoji: .maleSuperhero, skinTones: nil) + } else if rawValue == "đŸĻ¸đŸģ‍♂ī¸" { + self.init(baseEmoji: .maleSuperhero, skinTones: [.light]) + } else if rawValue == "đŸĻ¸đŸŧ‍♂ī¸" { + self.init(baseEmoji: .maleSuperhero, skinTones: [.mediumLight]) + } else if rawValue == "đŸĻ¸đŸŊ‍♂ī¸" { + self.init(baseEmoji: .maleSuperhero, skinTones: [.medium]) + } else if rawValue == "đŸĻ¸đŸžâ€â™‚ī¸" { + self.init(baseEmoji: .maleSuperhero, skinTones: [.mediumDark]) + } else if rawValue == "đŸĻ¸đŸŋ‍♂ī¸" { + self.init(baseEmoji: .maleSuperhero, skinTones: [.dark]) + } else if rawValue == "đŸĻ¸â€â™€ī¸" { + self.init(baseEmoji: .femaleSuperhero, skinTones: nil) + } else if rawValue == "đŸĻ¸đŸģ‍♀ī¸" { + self.init(baseEmoji: .femaleSuperhero, skinTones: [.light]) + } else if rawValue == "đŸĻ¸đŸŧ‍♀ī¸" { + self.init(baseEmoji: .femaleSuperhero, skinTones: [.mediumLight]) + } else if rawValue == "đŸĻ¸đŸŊ‍♀ī¸" { + self.init(baseEmoji: .femaleSuperhero, skinTones: [.medium]) + } else if rawValue == "đŸĻ¸đŸžâ€â™€ī¸" { + self.init(baseEmoji: .femaleSuperhero, skinTones: [.mediumDark]) + } else if rawValue == "đŸĻ¸đŸŋ‍♀ī¸" { + self.init(baseEmoji: .femaleSuperhero, skinTones: [.dark]) + } else if rawValue == "đŸĻš" { + self.init(baseEmoji: .supervillain, skinTones: nil) + } else if rawValue == "đŸĻšđŸģ" { + self.init(baseEmoji: .supervillain, skinTones: [.light]) + } else if rawValue == "đŸĻšđŸŧ" { + self.init(baseEmoji: .supervillain, skinTones: [.mediumLight]) + } else if rawValue == "đŸĻšđŸŊ" { + self.init(baseEmoji: .supervillain, skinTones: [.medium]) + } else if rawValue == "đŸĻšđŸž" { + self.init(baseEmoji: .supervillain, skinTones: [.mediumDark]) + } else if rawValue == "đŸĻšđŸŋ" { + self.init(baseEmoji: .supervillain, skinTones: [.dark]) + } else if rawValue == "đŸĻšâ€â™‚ī¸" { + self.init(baseEmoji: .maleSupervillain, skinTones: nil) + } else if rawValue == "đŸĻšđŸģ‍♂ī¸" { + self.init(baseEmoji: .maleSupervillain, skinTones: [.light]) + } else if rawValue == "đŸĻšđŸŧ‍♂ī¸" { + self.init(baseEmoji: .maleSupervillain, skinTones: [.mediumLight]) + } else if rawValue == "đŸĻšđŸŊ‍♂ī¸" { + self.init(baseEmoji: .maleSupervillain, skinTones: [.medium]) + } else if rawValue == "đŸĻšđŸžâ€â™‚ī¸" { + self.init(baseEmoji: .maleSupervillain, skinTones: [.mediumDark]) + } else if rawValue == "đŸĻšđŸŋ‍♂ī¸" { + self.init(baseEmoji: .maleSupervillain, skinTones: [.dark]) + } else if rawValue == "đŸĻšâ€â™€ī¸" { + self.init(baseEmoji: .femaleSupervillain, skinTones: nil) + } else if rawValue == "đŸĻšđŸģ‍♀ī¸" { + self.init(baseEmoji: .femaleSupervillain, skinTones: [.light]) + } else if rawValue == "đŸĻšđŸŧ‍♀ī¸" { + self.init(baseEmoji: .femaleSupervillain, skinTones: [.mediumLight]) + } else if rawValue == "đŸĻšđŸŊ‍♀ī¸" { + self.init(baseEmoji: .femaleSupervillain, skinTones: [.medium]) + } else if rawValue == "đŸĻšđŸžâ€â™€ī¸" { + self.init(baseEmoji: .femaleSupervillain, skinTones: [.mediumDark]) + } else if rawValue == "đŸĻšđŸŋ‍♀ī¸" { + self.init(baseEmoji: .femaleSupervillain, skinTones: [.dark]) + } else if rawValue == "🧙" { + self.init(baseEmoji: .mage, skinTones: nil) + } else if rawValue == "🧙đŸģ" { + self.init(baseEmoji: .mage, skinTones: [.light]) + } else if rawValue == "🧙đŸŧ" { + self.init(baseEmoji: .mage, skinTones: [.mediumLight]) + } else if rawValue == "🧙đŸŊ" { + self.init(baseEmoji: .mage, skinTones: [.medium]) + } else if rawValue == "🧙🏾" { + self.init(baseEmoji: .mage, skinTones: [.mediumDark]) + } else if rawValue == "🧙đŸŋ" { + self.init(baseEmoji: .mage, skinTones: [.dark]) + } else if rawValue == "🧙‍♂ī¸" { + self.init(baseEmoji: .maleMage, skinTones: nil) + } else if rawValue == "🧙đŸģ‍♂ī¸" { + self.init(baseEmoji: .maleMage, skinTones: [.light]) + } else if rawValue == "🧙đŸŧ‍♂ī¸" { + self.init(baseEmoji: .maleMage, skinTones: [.mediumLight]) + } else if rawValue == "🧙đŸŊ‍♂ī¸" { + self.init(baseEmoji: .maleMage, skinTones: [.medium]) + } else if rawValue == "🧙🏾‍♂ī¸" { + self.init(baseEmoji: .maleMage, skinTones: [.mediumDark]) + } else if rawValue == "🧙đŸŋ‍♂ī¸" { + self.init(baseEmoji: .maleMage, skinTones: [.dark]) + } else if rawValue == "🧙‍♀ī¸" { + self.init(baseEmoji: .femaleMage, skinTones: nil) + } else if rawValue == "🧙đŸģ‍♀ī¸" { + self.init(baseEmoji: .femaleMage, skinTones: [.light]) + } else if rawValue == "🧙đŸŧ‍♀ī¸" { + self.init(baseEmoji: .femaleMage, skinTones: [.mediumLight]) + } else if rawValue == "🧙đŸŊ‍♀ī¸" { + self.init(baseEmoji: .femaleMage, skinTones: [.medium]) + } else if rawValue == "🧙🏾‍♀ī¸" { + self.init(baseEmoji: .femaleMage, skinTones: [.mediumDark]) + } else if rawValue == "🧙đŸŋ‍♀ī¸" { + self.init(baseEmoji: .femaleMage, skinTones: [.dark]) + } else if rawValue == "🧚" { + self.init(baseEmoji: .fairy, skinTones: nil) + } else if rawValue == "🧚đŸģ" { + self.init(baseEmoji: .fairy, skinTones: [.light]) + } else if rawValue == "🧚đŸŧ" { + self.init(baseEmoji: .fairy, skinTones: [.mediumLight]) + } else if rawValue == "🧚đŸŊ" { + self.init(baseEmoji: .fairy, skinTones: [.medium]) + } else if rawValue == "🧚🏾" { + self.init(baseEmoji: .fairy, skinTones: [.mediumDark]) + } else if rawValue == "🧚đŸŋ" { + self.init(baseEmoji: .fairy, skinTones: [.dark]) + } else if rawValue == "🧚‍♂ī¸" { + self.init(baseEmoji: .maleFairy, skinTones: nil) + } else if rawValue == "🧚đŸģ‍♂ī¸" { + self.init(baseEmoji: .maleFairy, skinTones: [.light]) + } else if rawValue == "🧚đŸŧ‍♂ī¸" { + self.init(baseEmoji: .maleFairy, skinTones: [.mediumLight]) + } else if rawValue == "🧚đŸŊ‍♂ī¸" { + self.init(baseEmoji: .maleFairy, skinTones: [.medium]) + } else if rawValue == "🧚🏾‍♂ī¸" { + self.init(baseEmoji: .maleFairy, skinTones: [.mediumDark]) + } else if rawValue == "🧚đŸŋ‍♂ī¸" { + self.init(baseEmoji: .maleFairy, skinTones: [.dark]) + } else if rawValue == "🧚‍♀ī¸" { + self.init(baseEmoji: .femaleFairy, skinTones: nil) + } else if rawValue == "🧚đŸģ‍♀ī¸" { + self.init(baseEmoji: .femaleFairy, skinTones: [.light]) + } else if rawValue == "🧚đŸŧ‍♀ī¸" { + self.init(baseEmoji: .femaleFairy, skinTones: [.mediumLight]) + } else if rawValue == "🧚đŸŊ‍♀ī¸" { + self.init(baseEmoji: .femaleFairy, skinTones: [.medium]) + } else if rawValue == "🧚🏾‍♀ī¸" { + self.init(baseEmoji: .femaleFairy, skinTones: [.mediumDark]) + } else if rawValue == "🧚đŸŋ‍♀ī¸" { + self.init(baseEmoji: .femaleFairy, skinTones: [.dark]) + } else if rawValue == "🧛" { + self.init(baseEmoji: .vampire, skinTones: nil) + } else if rawValue == "🧛đŸģ" { + self.init(baseEmoji: .vampire, skinTones: [.light]) + } else if rawValue == "🧛đŸŧ" { + self.init(baseEmoji: .vampire, skinTones: [.mediumLight]) + } else if rawValue == "🧛đŸŊ" { + self.init(baseEmoji: .vampire, skinTones: [.medium]) + } else if rawValue == "🧛🏾" { + self.init(baseEmoji: .vampire, skinTones: [.mediumDark]) + } else if rawValue == "🧛đŸŋ" { + self.init(baseEmoji: .vampire, skinTones: [.dark]) + } else if rawValue == "🧛‍♂ī¸" { + self.init(baseEmoji: .maleVampire, skinTones: nil) + } else if rawValue == "🧛đŸģ‍♂ī¸" { + self.init(baseEmoji: .maleVampire, skinTones: [.light]) + } else if rawValue == "🧛đŸŧ‍♂ī¸" { + self.init(baseEmoji: .maleVampire, skinTones: [.mediumLight]) + } else if rawValue == "🧛đŸŊ‍♂ī¸" { + self.init(baseEmoji: .maleVampire, skinTones: [.medium]) + } else if rawValue == "🧛🏾‍♂ī¸" { + self.init(baseEmoji: .maleVampire, skinTones: [.mediumDark]) + } else if rawValue == "🧛đŸŋ‍♂ī¸" { + self.init(baseEmoji: .maleVampire, skinTones: [.dark]) + } else if rawValue == "🧛‍♀ī¸" { + self.init(baseEmoji: .femaleVampire, skinTones: nil) + } else if rawValue == "🧛đŸģ‍♀ī¸" { + self.init(baseEmoji: .femaleVampire, skinTones: [.light]) + } else if rawValue == "🧛đŸŧ‍♀ī¸" { + self.init(baseEmoji: .femaleVampire, skinTones: [.mediumLight]) + } else if rawValue == "🧛đŸŊ‍♀ī¸" { + self.init(baseEmoji: .femaleVampire, skinTones: [.medium]) + } else if rawValue == "🧛🏾‍♀ī¸" { + self.init(baseEmoji: .femaleVampire, skinTones: [.mediumDark]) + } else if rawValue == "🧛đŸŋ‍♀ī¸" { + self.init(baseEmoji: .femaleVampire, skinTones: [.dark]) + } else if rawValue == "🧜" { + self.init(baseEmoji: .merperson, skinTones: nil) + } else if rawValue == "🧜đŸģ" { + self.init(baseEmoji: .merperson, skinTones: [.light]) + } else if rawValue == "🧜đŸŧ" { + self.init(baseEmoji: .merperson, skinTones: [.mediumLight]) + } else if rawValue == "🧜đŸŊ" { + self.init(baseEmoji: .merperson, skinTones: [.medium]) + } else if rawValue == "🧜🏾" { + self.init(baseEmoji: .merperson, skinTones: [.mediumDark]) + } else if rawValue == "🧜đŸŋ" { + self.init(baseEmoji: .merperson, skinTones: [.dark]) + } else if rawValue == "🧜‍♂ī¸" { + self.init(baseEmoji: .merman, skinTones: nil) + } else if rawValue == "🧜đŸģ‍♂ī¸" { + self.init(baseEmoji: .merman, skinTones: [.light]) + } else if rawValue == "🧜đŸŧ‍♂ī¸" { + self.init(baseEmoji: .merman, skinTones: [.mediumLight]) + } else if rawValue == "🧜đŸŊ‍♂ī¸" { + self.init(baseEmoji: .merman, skinTones: [.medium]) + } else if rawValue == "🧜🏾‍♂ī¸" { + self.init(baseEmoji: .merman, skinTones: [.mediumDark]) + } else if rawValue == "🧜đŸŋ‍♂ī¸" { + self.init(baseEmoji: .merman, skinTones: [.dark]) + } else if rawValue == "🧜‍♀ī¸" { + self.init(baseEmoji: .mermaid, skinTones: nil) + } else if rawValue == "🧜đŸģ‍♀ī¸" { + self.init(baseEmoji: .mermaid, skinTones: [.light]) + } else if rawValue == "🧜đŸŧ‍♀ī¸" { + self.init(baseEmoji: .mermaid, skinTones: [.mediumLight]) + } else if rawValue == "🧜đŸŊ‍♀ī¸" { + self.init(baseEmoji: .mermaid, skinTones: [.medium]) + } else if rawValue == "🧜🏾‍♀ī¸" { + self.init(baseEmoji: .mermaid, skinTones: [.mediumDark]) + } else if rawValue == "🧜đŸŋ‍♀ī¸" { + self.init(baseEmoji: .mermaid, skinTones: [.dark]) + } else if rawValue == "🧝" { + self.init(baseEmoji: .elf, skinTones: nil) + } else if rawValue == "🧝đŸģ" { + self.init(baseEmoji: .elf, skinTones: [.light]) + } else if rawValue == "🧝đŸŧ" { + self.init(baseEmoji: .elf, skinTones: [.mediumLight]) + } else if rawValue == "🧝đŸŊ" { + self.init(baseEmoji: .elf, skinTones: [.medium]) + } else if rawValue == "🧝🏾" { + self.init(baseEmoji: .elf, skinTones: [.mediumDark]) + } else if rawValue == "🧝đŸŋ" { + self.init(baseEmoji: .elf, skinTones: [.dark]) + } else if rawValue == "🧝‍♂ī¸" { + self.init(baseEmoji: .maleElf, skinTones: nil) + } else if rawValue == "🧝đŸģ‍♂ī¸" { + self.init(baseEmoji: .maleElf, skinTones: [.light]) + } else if rawValue == "🧝đŸŧ‍♂ī¸" { + self.init(baseEmoji: .maleElf, skinTones: [.mediumLight]) + } else if rawValue == "🧝đŸŊ‍♂ī¸" { + self.init(baseEmoji: .maleElf, skinTones: [.medium]) + } else if rawValue == "🧝🏾‍♂ī¸" { + self.init(baseEmoji: .maleElf, skinTones: [.mediumDark]) + } else if rawValue == "🧝đŸŋ‍♂ī¸" { + self.init(baseEmoji: .maleElf, skinTones: [.dark]) + } else if rawValue == "🧝‍♀ī¸" { + self.init(baseEmoji: .femaleElf, skinTones: nil) + } else if rawValue == "🧝đŸģ‍♀ī¸" { + self.init(baseEmoji: .femaleElf, skinTones: [.light]) + } else if rawValue == "🧝đŸŧ‍♀ī¸" { + self.init(baseEmoji: .femaleElf, skinTones: [.mediumLight]) + } else if rawValue == "🧝đŸŊ‍♀ī¸" { + self.init(baseEmoji: .femaleElf, skinTones: [.medium]) + } else if rawValue == "🧝🏾‍♀ī¸" { + self.init(baseEmoji: .femaleElf, skinTones: [.mediumDark]) + } else if rawValue == "🧝đŸŋ‍♀ī¸" { + self.init(baseEmoji: .femaleElf, skinTones: [.dark]) + } else if rawValue == "🧞" { + self.init(baseEmoji: .genie, skinTones: nil) + } else if rawValue == "🧞‍♂ī¸" { + self.init(baseEmoji: .maleGenie, skinTones: nil) + } else if rawValue == "🧞‍♀ī¸" { + self.init(baseEmoji: .femaleGenie, skinTones: nil) + } else if rawValue == "🧟" { + self.init(baseEmoji: .zombie, skinTones: nil) + } else if rawValue == "🧟‍♂ī¸" { + self.init(baseEmoji: .maleZombie, skinTones: nil) + } else if rawValue == "🧟‍♀ī¸" { + self.init(baseEmoji: .femaleZombie, skinTones: nil) + } else if rawValue == "🧌" { + self.init(baseEmoji: .troll, skinTones: nil) + } else if rawValue == "💆" { + self.init(baseEmoji: .massage, skinTones: nil) + } else if rawValue == "💆đŸģ" { + self.init(baseEmoji: .massage, skinTones: [.light]) + } else if rawValue == "💆đŸŧ" { + self.init(baseEmoji: .massage, skinTones: [.mediumLight]) + } else if rawValue == "💆đŸŊ" { + self.init(baseEmoji: .massage, skinTones: [.medium]) + } else if rawValue == "💆🏾" { + self.init(baseEmoji: .massage, skinTones: [.mediumDark]) + } else if rawValue == "💆đŸŋ" { + self.init(baseEmoji: .massage, skinTones: [.dark]) + } else if rawValue == "💆‍♂ī¸" { + self.init(baseEmoji: .manGettingMassage, skinTones: nil) + } else if rawValue == "💆đŸģ‍♂ī¸" { + self.init(baseEmoji: .manGettingMassage, skinTones: [.light]) + } else if rawValue == "💆đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manGettingMassage, skinTones: [.mediumLight]) + } else if rawValue == "💆đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manGettingMassage, skinTones: [.medium]) + } else if rawValue == "💆🏾‍♂ī¸" { + self.init(baseEmoji: .manGettingMassage, skinTones: [.mediumDark]) + } else if rawValue == "💆đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manGettingMassage, skinTones: [.dark]) + } else if rawValue == "💆‍♀ī¸" { + self.init(baseEmoji: .womanGettingMassage, skinTones: nil) + } else if rawValue == "💆đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanGettingMassage, skinTones: [.light]) + } else if rawValue == "💆đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanGettingMassage, skinTones: [.mediumLight]) + } else if rawValue == "💆đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanGettingMassage, skinTones: [.medium]) + } else if rawValue == "💆🏾‍♀ī¸" { + self.init(baseEmoji: .womanGettingMassage, skinTones: [.mediumDark]) + } else if rawValue == "💆đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanGettingMassage, skinTones: [.dark]) + } else if rawValue == "💇" { + self.init(baseEmoji: .haircut, skinTones: nil) + } else if rawValue == "💇đŸģ" { + self.init(baseEmoji: .haircut, skinTones: [.light]) + } else if rawValue == "💇đŸŧ" { + self.init(baseEmoji: .haircut, skinTones: [.mediumLight]) + } else if rawValue == "💇đŸŊ" { + self.init(baseEmoji: .haircut, skinTones: [.medium]) + } else if rawValue == "💇🏾" { + self.init(baseEmoji: .haircut, skinTones: [.mediumDark]) + } else if rawValue == "💇đŸŋ" { + self.init(baseEmoji: .haircut, skinTones: [.dark]) + } else if rawValue == "💇‍♂ī¸" { + self.init(baseEmoji: .manGettingHaircut, skinTones: nil) + } else if rawValue == "💇đŸģ‍♂ī¸" { + self.init(baseEmoji: .manGettingHaircut, skinTones: [.light]) + } else if rawValue == "💇đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manGettingHaircut, skinTones: [.mediumLight]) + } else if rawValue == "💇đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manGettingHaircut, skinTones: [.medium]) + } else if rawValue == "💇🏾‍♂ī¸" { + self.init(baseEmoji: .manGettingHaircut, skinTones: [.mediumDark]) + } else if rawValue == "💇đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manGettingHaircut, skinTones: [.dark]) + } else if rawValue == "💇‍♀ī¸" { + self.init(baseEmoji: .womanGettingHaircut, skinTones: nil) + } else if rawValue == "💇đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanGettingHaircut, skinTones: [.light]) + } else if rawValue == "💇đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanGettingHaircut, skinTones: [.mediumLight]) + } else if rawValue == "💇đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanGettingHaircut, skinTones: [.medium]) + } else if rawValue == "💇🏾‍♀ī¸" { + self.init(baseEmoji: .womanGettingHaircut, skinTones: [.mediumDark]) + } else if rawValue == "💇đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanGettingHaircut, skinTones: [.dark]) + } else if rawValue == "đŸšļ" { + self.init(baseEmoji: .walking, skinTones: nil) + } else if rawValue == "đŸšļđŸģ" { + self.init(baseEmoji: .walking, skinTones: [.light]) + } else if rawValue == "đŸšļđŸŧ" { + self.init(baseEmoji: .walking, skinTones: [.mediumLight]) + } else if rawValue == "đŸšļđŸŊ" { + self.init(baseEmoji: .walking, skinTones: [.medium]) + } else if rawValue == "đŸšļ🏾" { + self.init(baseEmoji: .walking, skinTones: [.mediumDark]) + } else if rawValue == "đŸšļđŸŋ" { + self.init(baseEmoji: .walking, skinTones: [.dark]) + } else if rawValue == "đŸšļ‍♂ī¸" { + self.init(baseEmoji: .manWalking, skinTones: nil) + } else if rawValue == "đŸšļđŸģ‍♂ī¸" { + self.init(baseEmoji: .manWalking, skinTones: [.light]) + } else if rawValue == "đŸšļđŸŧ‍♂ī¸" { + self.init(baseEmoji: .manWalking, skinTones: [.mediumLight]) + } else if rawValue == "đŸšļđŸŊ‍♂ī¸" { + self.init(baseEmoji: .manWalking, skinTones: [.medium]) + } else if rawValue == "đŸšļ🏾‍♂ī¸" { + self.init(baseEmoji: .manWalking, skinTones: [.mediumDark]) + } else if rawValue == "đŸšļđŸŋ‍♂ī¸" { + self.init(baseEmoji: .manWalking, skinTones: [.dark]) + } else if rawValue == "đŸšļ‍♀ī¸" { + self.init(baseEmoji: .womanWalking, skinTones: nil) + } else if rawValue == "đŸšļđŸģ‍♀ī¸" { + self.init(baseEmoji: .womanWalking, skinTones: [.light]) + } else if rawValue == "đŸšļđŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanWalking, skinTones: [.mediumLight]) + } else if rawValue == "đŸšļđŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanWalking, skinTones: [.medium]) + } else if rawValue == "đŸšļ🏾‍♀ī¸" { + self.init(baseEmoji: .womanWalking, skinTones: [.mediumDark]) + } else if rawValue == "đŸšļđŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanWalking, skinTones: [.dark]) + } else if rawValue == "🧍" { + self.init(baseEmoji: .standingPerson, skinTones: nil) + } else if rawValue == "🧍đŸģ" { + self.init(baseEmoji: .standingPerson, skinTones: [.light]) + } else if rawValue == "🧍đŸŧ" { + self.init(baseEmoji: .standingPerson, skinTones: [.mediumLight]) + } else if rawValue == "🧍đŸŊ" { + self.init(baseEmoji: .standingPerson, skinTones: [.medium]) + } else if rawValue == "🧍🏾" { + self.init(baseEmoji: .standingPerson, skinTones: [.mediumDark]) + } else if rawValue == "🧍đŸŋ" { + self.init(baseEmoji: .standingPerson, skinTones: [.dark]) + } else if rawValue == "🧍‍♂ī¸" { + self.init(baseEmoji: .manStanding, skinTones: nil) + } else if rawValue == "🧍đŸģ‍♂ī¸" { + self.init(baseEmoji: .manStanding, skinTones: [.light]) + } else if rawValue == "🧍đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manStanding, skinTones: [.mediumLight]) + } else if rawValue == "🧍đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manStanding, skinTones: [.medium]) + } else if rawValue == "🧍🏾‍♂ī¸" { + self.init(baseEmoji: .manStanding, skinTones: [.mediumDark]) + } else if rawValue == "🧍đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manStanding, skinTones: [.dark]) + } else if rawValue == "🧍‍♀ī¸" { + self.init(baseEmoji: .womanStanding, skinTones: nil) + } else if rawValue == "🧍đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanStanding, skinTones: [.light]) + } else if rawValue == "🧍đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanStanding, skinTones: [.mediumLight]) + } else if rawValue == "🧍đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanStanding, skinTones: [.medium]) + } else if rawValue == "🧍🏾‍♀ī¸" { + self.init(baseEmoji: .womanStanding, skinTones: [.mediumDark]) + } else if rawValue == "🧍đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanStanding, skinTones: [.dark]) + } else if rawValue == "🧎" { + self.init(baseEmoji: .kneelingPerson, skinTones: nil) + } else if rawValue == "🧎đŸģ" { + self.init(baseEmoji: .kneelingPerson, skinTones: [.light]) + } else if rawValue == "🧎đŸŧ" { + self.init(baseEmoji: .kneelingPerson, skinTones: [.mediumLight]) + } else if rawValue == "🧎đŸŊ" { + self.init(baseEmoji: .kneelingPerson, skinTones: [.medium]) + } else if rawValue == "🧎🏾" { + self.init(baseEmoji: .kneelingPerson, skinTones: [.mediumDark]) + } else if rawValue == "🧎đŸŋ" { + self.init(baseEmoji: .kneelingPerson, skinTones: [.dark]) + } else if rawValue == "🧎‍♂ī¸" { + self.init(baseEmoji: .manKneeling, skinTones: nil) + } else if rawValue == "🧎đŸģ‍♂ī¸" { + self.init(baseEmoji: .manKneeling, skinTones: [.light]) + } else if rawValue == "🧎đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manKneeling, skinTones: [.mediumLight]) + } else if rawValue == "🧎đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manKneeling, skinTones: [.medium]) + } else if rawValue == "🧎🏾‍♂ī¸" { + self.init(baseEmoji: .manKneeling, skinTones: [.mediumDark]) + } else if rawValue == "🧎đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manKneeling, skinTones: [.dark]) + } else if rawValue == "🧎‍♀ī¸" { + self.init(baseEmoji: .womanKneeling, skinTones: nil) + } else if rawValue == "🧎đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanKneeling, skinTones: [.light]) + } else if rawValue == "🧎đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanKneeling, skinTones: [.mediumLight]) + } else if rawValue == "🧎đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanKneeling, skinTones: [.medium]) + } else if rawValue == "🧎🏾‍♀ī¸" { + self.init(baseEmoji: .womanKneeling, skinTones: [.mediumDark]) + } else if rawValue == "🧎đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanKneeling, skinTones: [.dark]) + } else if rawValue == "🧑‍đŸĻ¯" { + self.init(baseEmoji: .personWithProbingCane, skinTones: nil) + } else if rawValue == "🧑đŸģ‍đŸĻ¯" { + self.init(baseEmoji: .personWithProbingCane, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍đŸĻ¯" { + self.init(baseEmoji: .personWithProbingCane, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍đŸĻ¯" { + self.init(baseEmoji: .personWithProbingCane, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍đŸĻ¯" { + self.init(baseEmoji: .personWithProbingCane, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍đŸĻ¯" { + self.init(baseEmoji: .personWithProbingCane, skinTones: [.dark]) + } else if rawValue == "👨‍đŸĻ¯" { + self.init(baseEmoji: .manWithProbingCane, skinTones: nil) + } else if rawValue == "👨đŸģ‍đŸĻ¯" { + self.init(baseEmoji: .manWithProbingCane, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍đŸĻ¯" { + self.init(baseEmoji: .manWithProbingCane, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍đŸĻ¯" { + self.init(baseEmoji: .manWithProbingCane, skinTones: [.medium]) + } else if rawValue == "👨🏾‍đŸĻ¯" { + self.init(baseEmoji: .manWithProbingCane, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍đŸĻ¯" { + self.init(baseEmoji: .manWithProbingCane, skinTones: [.dark]) + } else if rawValue == "👩‍đŸĻ¯" { + self.init(baseEmoji: .womanWithProbingCane, skinTones: nil) + } else if rawValue == "👩đŸģ‍đŸĻ¯" { + self.init(baseEmoji: .womanWithProbingCane, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍đŸĻ¯" { + self.init(baseEmoji: .womanWithProbingCane, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍đŸĻ¯" { + self.init(baseEmoji: .womanWithProbingCane, skinTones: [.medium]) + } else if rawValue == "👩🏾‍đŸĻ¯" { + self.init(baseEmoji: .womanWithProbingCane, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍đŸĻ¯" { + self.init(baseEmoji: .womanWithProbingCane, skinTones: [.dark]) + } else if rawValue == "🧑‍đŸĻŧ" { + self.init(baseEmoji: .personInMotorizedWheelchair, skinTones: nil) + } else if rawValue == "🧑đŸģ‍đŸĻŧ" { + self.init(baseEmoji: .personInMotorizedWheelchair, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍đŸĻŧ" { + self.init(baseEmoji: .personInMotorizedWheelchair, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍đŸĻŧ" { + self.init(baseEmoji: .personInMotorizedWheelchair, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍đŸĻŧ" { + self.init(baseEmoji: .personInMotorizedWheelchair, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍đŸĻŧ" { + self.init(baseEmoji: .personInMotorizedWheelchair, skinTones: [.dark]) + } else if rawValue == "👨‍đŸĻŧ" { + self.init(baseEmoji: .manInMotorizedWheelchair, skinTones: nil) + } else if rawValue == "👨đŸģ‍đŸĻŧ" { + self.init(baseEmoji: .manInMotorizedWheelchair, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍đŸĻŧ" { + self.init(baseEmoji: .manInMotorizedWheelchair, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍đŸĻŧ" { + self.init(baseEmoji: .manInMotorizedWheelchair, skinTones: [.medium]) + } else if rawValue == "👨🏾‍đŸĻŧ" { + self.init(baseEmoji: .manInMotorizedWheelchair, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍đŸĻŧ" { + self.init(baseEmoji: .manInMotorizedWheelchair, skinTones: [.dark]) + } else if rawValue == "👩‍đŸĻŧ" { + self.init(baseEmoji: .womanInMotorizedWheelchair, skinTones: nil) + } else if rawValue == "👩đŸģ‍đŸĻŧ" { + self.init(baseEmoji: .womanInMotorizedWheelchair, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍đŸĻŧ" { + self.init(baseEmoji: .womanInMotorizedWheelchair, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍đŸĻŧ" { + self.init(baseEmoji: .womanInMotorizedWheelchair, skinTones: [.medium]) + } else if rawValue == "👩🏾‍đŸĻŧ" { + self.init(baseEmoji: .womanInMotorizedWheelchair, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍đŸĻŧ" { + self.init(baseEmoji: .womanInMotorizedWheelchair, skinTones: [.dark]) + } else if rawValue == "🧑‍đŸĻŊ" { + self.init(baseEmoji: .personInManualWheelchair, skinTones: nil) + } else if rawValue == "🧑đŸģ‍đŸĻŊ" { + self.init(baseEmoji: .personInManualWheelchair, skinTones: [.light]) + } else if rawValue == "🧑đŸŧ‍đŸĻŊ" { + self.init(baseEmoji: .personInManualWheelchair, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŊ‍đŸĻŊ" { + self.init(baseEmoji: .personInManualWheelchair, skinTones: [.medium]) + } else if rawValue == "🧑🏾‍đŸĻŊ" { + self.init(baseEmoji: .personInManualWheelchair, skinTones: [.mediumDark]) + } else if rawValue == "🧑đŸŋ‍đŸĻŊ" { + self.init(baseEmoji: .personInManualWheelchair, skinTones: [.dark]) + } else if rawValue == "👨‍đŸĻŊ" { + self.init(baseEmoji: .manInManualWheelchair, skinTones: nil) + } else if rawValue == "👨đŸģ‍đŸĻŊ" { + self.init(baseEmoji: .manInManualWheelchair, skinTones: [.light]) + } else if rawValue == "👨đŸŧ‍đŸĻŊ" { + self.init(baseEmoji: .manInManualWheelchair, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŊ‍đŸĻŊ" { + self.init(baseEmoji: .manInManualWheelchair, skinTones: [.medium]) + } else if rawValue == "👨🏾‍đŸĻŊ" { + self.init(baseEmoji: .manInManualWheelchair, skinTones: [.mediumDark]) + } else if rawValue == "👨đŸŋ‍đŸĻŊ" { + self.init(baseEmoji: .manInManualWheelchair, skinTones: [.dark]) + } else if rawValue == "👩‍đŸĻŊ" { + self.init(baseEmoji: .womanInManualWheelchair, skinTones: nil) + } else if rawValue == "👩đŸģ‍đŸĻŊ" { + self.init(baseEmoji: .womanInManualWheelchair, skinTones: [.light]) + } else if rawValue == "👩đŸŧ‍đŸĻŊ" { + self.init(baseEmoji: .womanInManualWheelchair, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŊ‍đŸĻŊ" { + self.init(baseEmoji: .womanInManualWheelchair, skinTones: [.medium]) + } else if rawValue == "👩🏾‍đŸĻŊ" { + self.init(baseEmoji: .womanInManualWheelchair, skinTones: [.mediumDark]) + } else if rawValue == "👩đŸŋ‍đŸĻŊ" { + self.init(baseEmoji: .womanInManualWheelchair, skinTones: [.dark]) + } else if rawValue == "🏃" { + self.init(baseEmoji: .runner, skinTones: nil) + } else if rawValue == "🏃đŸģ" { + self.init(baseEmoji: .runner, skinTones: [.light]) + } else if rawValue == "🏃đŸŧ" { + self.init(baseEmoji: .runner, skinTones: [.mediumLight]) + } else if rawValue == "🏃đŸŊ" { + self.init(baseEmoji: .runner, skinTones: [.medium]) + } else if rawValue == "🏃🏾" { + self.init(baseEmoji: .runner, skinTones: [.mediumDark]) + } else if rawValue == "🏃đŸŋ" { + self.init(baseEmoji: .runner, skinTones: [.dark]) + } else if rawValue == "🏃‍♂ī¸" { + self.init(baseEmoji: .manRunning, skinTones: nil) + } else if rawValue == "🏃đŸģ‍♂ī¸" { + self.init(baseEmoji: .manRunning, skinTones: [.light]) + } else if rawValue == "🏃đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manRunning, skinTones: [.mediumLight]) + } else if rawValue == "🏃đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manRunning, skinTones: [.medium]) + } else if rawValue == "🏃🏾‍♂ī¸" { + self.init(baseEmoji: .manRunning, skinTones: [.mediumDark]) + } else if rawValue == "🏃đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manRunning, skinTones: [.dark]) + } else if rawValue == "🏃‍♀ī¸" { + self.init(baseEmoji: .womanRunning, skinTones: nil) + } else if rawValue == "🏃đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanRunning, skinTones: [.light]) + } else if rawValue == "🏃đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanRunning, skinTones: [.mediumLight]) + } else if rawValue == "🏃đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanRunning, skinTones: [.medium]) + } else if rawValue == "🏃🏾‍♀ī¸" { + self.init(baseEmoji: .womanRunning, skinTones: [.mediumDark]) + } else if rawValue == "🏃đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanRunning, skinTones: [.dark]) + } else if rawValue == "💃" { + self.init(baseEmoji: .dancer, skinTones: nil) + } else if rawValue == "💃đŸģ" { + self.init(baseEmoji: .dancer, skinTones: [.light]) + } else if rawValue == "💃đŸŧ" { + self.init(baseEmoji: .dancer, skinTones: [.mediumLight]) + } else if rawValue == "💃đŸŊ" { + self.init(baseEmoji: .dancer, skinTones: [.medium]) + } else if rawValue == "💃🏾" { + self.init(baseEmoji: .dancer, skinTones: [.mediumDark]) + } else if rawValue == "💃đŸŋ" { + self.init(baseEmoji: .dancer, skinTones: [.dark]) + } else if rawValue == "đŸ•ē" { + self.init(baseEmoji: .manDancing, skinTones: nil) + } else if rawValue == "đŸ•ēđŸģ" { + self.init(baseEmoji: .manDancing, skinTones: [.light]) + } else if rawValue == "đŸ•ēđŸŧ" { + self.init(baseEmoji: .manDancing, skinTones: [.mediumLight]) + } else if rawValue == "đŸ•ēđŸŊ" { + self.init(baseEmoji: .manDancing, skinTones: [.medium]) + } else if rawValue == "đŸ•ē🏾" { + self.init(baseEmoji: .manDancing, skinTones: [.mediumDark]) + } else if rawValue == "đŸ•ēđŸŋ" { + self.init(baseEmoji: .manDancing, skinTones: [.dark]) + } else if rawValue == "🕴ī¸" { + self.init(baseEmoji: .manInBusinessSuitLevitating, skinTones: nil) + } else if rawValue == "🕴đŸģ" { + self.init(baseEmoji: .manInBusinessSuitLevitating, skinTones: [.light]) + } else if rawValue == "🕴đŸŧ" { + self.init(baseEmoji: .manInBusinessSuitLevitating, skinTones: [.mediumLight]) + } else if rawValue == "🕴đŸŊ" { + self.init(baseEmoji: .manInBusinessSuitLevitating, skinTones: [.medium]) + } else if rawValue == "🕴🏾" { + self.init(baseEmoji: .manInBusinessSuitLevitating, skinTones: [.mediumDark]) + } else if rawValue == "🕴đŸŋ" { + self.init(baseEmoji: .manInBusinessSuitLevitating, skinTones: [.dark]) + } else if rawValue == "đŸ‘¯" { + self.init(baseEmoji: .dancers, skinTones: nil) + } else if rawValue == "đŸ‘¯â€â™‚ī¸" { + self.init(baseEmoji: .menWithBunnyEarsPartying, skinTones: nil) + } else if rawValue == "đŸ‘¯â€â™€ī¸" { + self.init(baseEmoji: .womenWithBunnyEarsPartying, skinTones: nil) + } else if rawValue == "🧖" { + self.init(baseEmoji: .personInSteamyRoom, skinTones: nil) + } else if rawValue == "🧖đŸģ" { + self.init(baseEmoji: .personInSteamyRoom, skinTones: [.light]) + } else if rawValue == "🧖đŸŧ" { + self.init(baseEmoji: .personInSteamyRoom, skinTones: [.mediumLight]) + } else if rawValue == "🧖đŸŊ" { + self.init(baseEmoji: .personInSteamyRoom, skinTones: [.medium]) + } else if rawValue == "🧖🏾" { + self.init(baseEmoji: .personInSteamyRoom, skinTones: [.mediumDark]) + } else if rawValue == "🧖đŸŋ" { + self.init(baseEmoji: .personInSteamyRoom, skinTones: [.dark]) + } else if rawValue == "🧖‍♂ī¸" { + self.init(baseEmoji: .manInSteamyRoom, skinTones: nil) + } else if rawValue == "🧖đŸģ‍♂ī¸" { + self.init(baseEmoji: .manInSteamyRoom, skinTones: [.light]) + } else if rawValue == "🧖đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manInSteamyRoom, skinTones: [.mediumLight]) + } else if rawValue == "🧖đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manInSteamyRoom, skinTones: [.medium]) + } else if rawValue == "🧖🏾‍♂ī¸" { + self.init(baseEmoji: .manInSteamyRoom, skinTones: [.mediumDark]) + } else if rawValue == "🧖đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manInSteamyRoom, skinTones: [.dark]) + } else if rawValue == "🧖‍♀ī¸" { + self.init(baseEmoji: .womanInSteamyRoom, skinTones: nil) + } else if rawValue == "🧖đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanInSteamyRoom, skinTones: [.light]) + } else if rawValue == "🧖đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanInSteamyRoom, skinTones: [.mediumLight]) + } else if rawValue == "🧖đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanInSteamyRoom, skinTones: [.medium]) + } else if rawValue == "🧖🏾‍♀ī¸" { + self.init(baseEmoji: .womanInSteamyRoom, skinTones: [.mediumDark]) + } else if rawValue == "🧖đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanInSteamyRoom, skinTones: [.dark]) + } else if rawValue == "🧗" { + self.init(baseEmoji: .personClimbing, skinTones: nil) + } else if rawValue == "🧗đŸģ" { + self.init(baseEmoji: .personClimbing, skinTones: [.light]) + } else if rawValue == "🧗đŸŧ" { + self.init(baseEmoji: .personClimbing, skinTones: [.mediumLight]) + } else if rawValue == "🧗đŸŊ" { + self.init(baseEmoji: .personClimbing, skinTones: [.medium]) + } else if rawValue == "🧗🏾" { + self.init(baseEmoji: .personClimbing, skinTones: [.mediumDark]) + } else if rawValue == "🧗đŸŋ" { + self.init(baseEmoji: .personClimbing, skinTones: [.dark]) + } else if rawValue == "🧗‍♂ī¸" { + self.init(baseEmoji: .manClimbing, skinTones: nil) + } else if rawValue == "🧗đŸģ‍♂ī¸" { + self.init(baseEmoji: .manClimbing, skinTones: [.light]) + } else if rawValue == "🧗đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manClimbing, skinTones: [.mediumLight]) + } else if rawValue == "🧗đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manClimbing, skinTones: [.medium]) + } else if rawValue == "🧗🏾‍♂ī¸" { + self.init(baseEmoji: .manClimbing, skinTones: [.mediumDark]) + } else if rawValue == "🧗đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manClimbing, skinTones: [.dark]) + } else if rawValue == "🧗‍♀ī¸" { + self.init(baseEmoji: .womanClimbing, skinTones: nil) + } else if rawValue == "🧗đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanClimbing, skinTones: [.light]) + } else if rawValue == "🧗đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanClimbing, skinTones: [.mediumLight]) + } else if rawValue == "🧗đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanClimbing, skinTones: [.medium]) + } else if rawValue == "🧗🏾‍♀ī¸" { + self.init(baseEmoji: .womanClimbing, skinTones: [.mediumDark]) + } else if rawValue == "🧗đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanClimbing, skinTones: [.dark]) + } else if rawValue == "đŸ¤ē" { + self.init(baseEmoji: .fencer, skinTones: nil) + } else if rawValue == "🏇" { + self.init(baseEmoji: .horseRacing, skinTones: nil) + } else if rawValue == "🏇đŸģ" { + self.init(baseEmoji: .horseRacing, skinTones: [.light]) + } else if rawValue == "🏇đŸŧ" { + self.init(baseEmoji: .horseRacing, skinTones: [.mediumLight]) + } else if rawValue == "🏇đŸŊ" { + self.init(baseEmoji: .horseRacing, skinTones: [.medium]) + } else if rawValue == "🏇🏾" { + self.init(baseEmoji: .horseRacing, skinTones: [.mediumDark]) + } else if rawValue == "🏇đŸŋ" { + self.init(baseEmoji: .horseRacing, skinTones: [.dark]) + } else if rawValue == "⛷ī¸" { + self.init(baseEmoji: .skier, skinTones: nil) + } else if rawValue == "🏂" { + self.init(baseEmoji: .snowboarder, skinTones: nil) + } else if rawValue == "🏂đŸģ" { + self.init(baseEmoji: .snowboarder, skinTones: [.light]) + } else if rawValue == "🏂đŸŧ" { + self.init(baseEmoji: .snowboarder, skinTones: [.mediumLight]) + } else if rawValue == "🏂đŸŊ" { + self.init(baseEmoji: .snowboarder, skinTones: [.medium]) + } else if rawValue == "🏂🏾" { + self.init(baseEmoji: .snowboarder, skinTones: [.mediumDark]) + } else if rawValue == "🏂đŸŋ" { + self.init(baseEmoji: .snowboarder, skinTones: [.dark]) + } else if rawValue == "🏌ī¸" { + self.init(baseEmoji: .golfer, skinTones: nil) + } else if rawValue == "🏌đŸģ" { + self.init(baseEmoji: .golfer, skinTones: [.light]) + } else if rawValue == "🏌đŸŧ" { + self.init(baseEmoji: .golfer, skinTones: [.mediumLight]) + } else if rawValue == "🏌đŸŊ" { + self.init(baseEmoji: .golfer, skinTones: [.medium]) + } else if rawValue == "🏌🏾" { + self.init(baseEmoji: .golfer, skinTones: [.mediumDark]) + } else if rawValue == "🏌đŸŋ" { + self.init(baseEmoji: .golfer, skinTones: [.dark]) + } else if rawValue == "🏌ī¸â€â™‚ī¸" { + self.init(baseEmoji: .manGolfing, skinTones: nil) + } else if rawValue == "🏌đŸģ‍♂ī¸" { + self.init(baseEmoji: .manGolfing, skinTones: [.light]) + } else if rawValue == "🏌đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manGolfing, skinTones: [.mediumLight]) + } else if rawValue == "🏌đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manGolfing, skinTones: [.medium]) + } else if rawValue == "🏌🏾‍♂ī¸" { + self.init(baseEmoji: .manGolfing, skinTones: [.mediumDark]) + } else if rawValue == "🏌đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manGolfing, skinTones: [.dark]) + } else if rawValue == "🏌ī¸â€â™€ī¸" { + self.init(baseEmoji: .womanGolfing, skinTones: nil) + } else if rawValue == "🏌đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanGolfing, skinTones: [.light]) + } else if rawValue == "🏌đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanGolfing, skinTones: [.mediumLight]) + } else if rawValue == "🏌đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanGolfing, skinTones: [.medium]) + } else if rawValue == "🏌🏾‍♀ī¸" { + self.init(baseEmoji: .womanGolfing, skinTones: [.mediumDark]) + } else if rawValue == "🏌đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanGolfing, skinTones: [.dark]) + } else if rawValue == "🏄" { + self.init(baseEmoji: .surfer, skinTones: nil) + } else if rawValue == "🏄đŸģ" { + self.init(baseEmoji: .surfer, skinTones: [.light]) + } else if rawValue == "🏄đŸŧ" { + self.init(baseEmoji: .surfer, skinTones: [.mediumLight]) + } else if rawValue == "🏄đŸŊ" { + self.init(baseEmoji: .surfer, skinTones: [.medium]) + } else if rawValue == "🏄🏾" { + self.init(baseEmoji: .surfer, skinTones: [.mediumDark]) + } else if rawValue == "🏄đŸŋ" { + self.init(baseEmoji: .surfer, skinTones: [.dark]) + } else if rawValue == "🏄‍♂ī¸" { + self.init(baseEmoji: .manSurfing, skinTones: nil) + } else if rawValue == "🏄đŸģ‍♂ī¸" { + self.init(baseEmoji: .manSurfing, skinTones: [.light]) + } else if rawValue == "🏄đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manSurfing, skinTones: [.mediumLight]) + } else if rawValue == "🏄đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manSurfing, skinTones: [.medium]) + } else if rawValue == "🏄🏾‍♂ī¸" { + self.init(baseEmoji: .manSurfing, skinTones: [.mediumDark]) + } else if rawValue == "🏄đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manSurfing, skinTones: [.dark]) + } else if rawValue == "🏄‍♀ī¸" { + self.init(baseEmoji: .womanSurfing, skinTones: nil) + } else if rawValue == "🏄đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanSurfing, skinTones: [.light]) + } else if rawValue == "🏄đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanSurfing, skinTones: [.mediumLight]) + } else if rawValue == "🏄đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanSurfing, skinTones: [.medium]) + } else if rawValue == "🏄🏾‍♀ī¸" { + self.init(baseEmoji: .womanSurfing, skinTones: [.mediumDark]) + } else if rawValue == "🏄đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanSurfing, skinTones: [.dark]) + } else if rawValue == "đŸšŖ" { + self.init(baseEmoji: .rowboat, skinTones: nil) + } else if rawValue == "đŸšŖđŸģ" { + self.init(baseEmoji: .rowboat, skinTones: [.light]) + } else if rawValue == "đŸšŖđŸŧ" { + self.init(baseEmoji: .rowboat, skinTones: [.mediumLight]) + } else if rawValue == "đŸšŖđŸŊ" { + self.init(baseEmoji: .rowboat, skinTones: [.medium]) + } else if rawValue == "đŸšŖ🏾" { + self.init(baseEmoji: .rowboat, skinTones: [.mediumDark]) + } else if rawValue == "đŸšŖđŸŋ" { + self.init(baseEmoji: .rowboat, skinTones: [.dark]) + } else if rawValue == "đŸšŖ‍♂ī¸" { + self.init(baseEmoji: .manRowingBoat, skinTones: nil) + } else if rawValue == "đŸšŖđŸģ‍♂ī¸" { + self.init(baseEmoji: .manRowingBoat, skinTones: [.light]) + } else if rawValue == "đŸšŖđŸŧ‍♂ī¸" { + self.init(baseEmoji: .manRowingBoat, skinTones: [.mediumLight]) + } else if rawValue == "đŸšŖđŸŊ‍♂ī¸" { + self.init(baseEmoji: .manRowingBoat, skinTones: [.medium]) + } else if rawValue == "đŸšŖ🏾‍♂ī¸" { + self.init(baseEmoji: .manRowingBoat, skinTones: [.mediumDark]) + } else if rawValue == "đŸšŖđŸŋ‍♂ī¸" { + self.init(baseEmoji: .manRowingBoat, skinTones: [.dark]) + } else if rawValue == "đŸšŖ‍♀ī¸" { + self.init(baseEmoji: .womanRowingBoat, skinTones: nil) + } else if rawValue == "đŸšŖđŸģ‍♀ī¸" { + self.init(baseEmoji: .womanRowingBoat, skinTones: [.light]) + } else if rawValue == "đŸšŖđŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanRowingBoat, skinTones: [.mediumLight]) + } else if rawValue == "đŸšŖđŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanRowingBoat, skinTones: [.medium]) + } else if rawValue == "đŸšŖ🏾‍♀ī¸" { + self.init(baseEmoji: .womanRowingBoat, skinTones: [.mediumDark]) + } else if rawValue == "đŸšŖđŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanRowingBoat, skinTones: [.dark]) + } else if rawValue == "🏊" { + self.init(baseEmoji: .swimmer, skinTones: nil) + } else if rawValue == "🏊đŸģ" { + self.init(baseEmoji: .swimmer, skinTones: [.light]) + } else if rawValue == "🏊đŸŧ" { + self.init(baseEmoji: .swimmer, skinTones: [.mediumLight]) + } else if rawValue == "🏊đŸŊ" { + self.init(baseEmoji: .swimmer, skinTones: [.medium]) + } else if rawValue == "🏊🏾" { + self.init(baseEmoji: .swimmer, skinTones: [.mediumDark]) + } else if rawValue == "🏊đŸŋ" { + self.init(baseEmoji: .swimmer, skinTones: [.dark]) + } else if rawValue == "🏊‍♂ī¸" { + self.init(baseEmoji: .manSwimming, skinTones: nil) + } else if rawValue == "🏊đŸģ‍♂ī¸" { + self.init(baseEmoji: .manSwimming, skinTones: [.light]) + } else if rawValue == "🏊đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manSwimming, skinTones: [.mediumLight]) + } else if rawValue == "🏊đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manSwimming, skinTones: [.medium]) + } else if rawValue == "🏊🏾‍♂ī¸" { + self.init(baseEmoji: .manSwimming, skinTones: [.mediumDark]) + } else if rawValue == "🏊đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manSwimming, skinTones: [.dark]) + } else if rawValue == "🏊‍♀ī¸" { + self.init(baseEmoji: .womanSwimming, skinTones: nil) + } else if rawValue == "🏊đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanSwimming, skinTones: [.light]) + } else if rawValue == "🏊đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanSwimming, skinTones: [.mediumLight]) + } else if rawValue == "🏊đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanSwimming, skinTones: [.medium]) + } else if rawValue == "🏊🏾‍♀ī¸" { + self.init(baseEmoji: .womanSwimming, skinTones: [.mediumDark]) + } else if rawValue == "🏊đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanSwimming, skinTones: [.dark]) + } else if rawValue == "⛹ī¸" { + self.init(baseEmoji: .personWithBall, skinTones: nil) + } else if rawValue == "⛹đŸģ" { + self.init(baseEmoji: .personWithBall, skinTones: [.light]) + } else if rawValue == "⛹đŸŧ" { + self.init(baseEmoji: .personWithBall, skinTones: [.mediumLight]) + } else if rawValue == "⛹đŸŊ" { + self.init(baseEmoji: .personWithBall, skinTones: [.medium]) + } else if rawValue == "⛹🏾" { + self.init(baseEmoji: .personWithBall, skinTones: [.mediumDark]) + } else if rawValue == "⛹đŸŋ" { + self.init(baseEmoji: .personWithBall, skinTones: [.dark]) + } else if rawValue == "⛹ī¸â€â™‚ī¸" { + self.init(baseEmoji: .manBouncingBall, skinTones: nil) + } else if rawValue == "⛹đŸģ‍♂ī¸" { + self.init(baseEmoji: .manBouncingBall, skinTones: [.light]) + } else if rawValue == "⛹đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manBouncingBall, skinTones: [.mediumLight]) + } else if rawValue == "⛹đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manBouncingBall, skinTones: [.medium]) + } else if rawValue == "⛹🏾‍♂ī¸" { + self.init(baseEmoji: .manBouncingBall, skinTones: [.mediumDark]) + } else if rawValue == "⛹đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manBouncingBall, skinTones: [.dark]) + } else if rawValue == "⛹ī¸â€â™€ī¸" { + self.init(baseEmoji: .womanBouncingBall, skinTones: nil) + } else if rawValue == "⛹đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanBouncingBall, skinTones: [.light]) + } else if rawValue == "⛹đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanBouncingBall, skinTones: [.mediumLight]) + } else if rawValue == "⛹đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanBouncingBall, skinTones: [.medium]) + } else if rawValue == "⛹🏾‍♀ī¸" { + self.init(baseEmoji: .womanBouncingBall, skinTones: [.mediumDark]) + } else if rawValue == "⛹đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanBouncingBall, skinTones: [.dark]) + } else if rawValue == "🏋ī¸" { + self.init(baseEmoji: .weightLifter, skinTones: nil) + } else if rawValue == "🏋đŸģ" { + self.init(baseEmoji: .weightLifter, skinTones: [.light]) + } else if rawValue == "🏋đŸŧ" { + self.init(baseEmoji: .weightLifter, skinTones: [.mediumLight]) + } else if rawValue == "🏋đŸŊ" { + self.init(baseEmoji: .weightLifter, skinTones: [.medium]) + } else if rawValue == "🏋🏾" { + self.init(baseEmoji: .weightLifter, skinTones: [.mediumDark]) + } else if rawValue == "🏋đŸŋ" { + self.init(baseEmoji: .weightLifter, skinTones: [.dark]) + } else if rawValue == "🏋ī¸â€â™‚ī¸" { + self.init(baseEmoji: .manLiftingWeights, skinTones: nil) + } else if rawValue == "🏋đŸģ‍♂ī¸" { + self.init(baseEmoji: .manLiftingWeights, skinTones: [.light]) + } else if rawValue == "🏋đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manLiftingWeights, skinTones: [.mediumLight]) + } else if rawValue == "🏋đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manLiftingWeights, skinTones: [.medium]) + } else if rawValue == "🏋🏾‍♂ī¸" { + self.init(baseEmoji: .manLiftingWeights, skinTones: [.mediumDark]) + } else if rawValue == "🏋đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manLiftingWeights, skinTones: [.dark]) + } else if rawValue == "🏋ī¸â€â™€ī¸" { + self.init(baseEmoji: .womanLiftingWeights, skinTones: nil) + } else if rawValue == "🏋đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanLiftingWeights, skinTones: [.light]) + } else if rawValue == "🏋đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanLiftingWeights, skinTones: [.mediumLight]) + } else if rawValue == "🏋đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanLiftingWeights, skinTones: [.medium]) + } else if rawValue == "🏋🏾‍♀ī¸" { + self.init(baseEmoji: .womanLiftingWeights, skinTones: [.mediumDark]) + } else if rawValue == "🏋đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanLiftingWeights, skinTones: [.dark]) + } else if rawValue == "🚴" { + self.init(baseEmoji: .bicyclist, skinTones: nil) + } else if rawValue == "🚴đŸģ" { + self.init(baseEmoji: .bicyclist, skinTones: [.light]) + } else if rawValue == "🚴đŸŧ" { + self.init(baseEmoji: .bicyclist, skinTones: [.mediumLight]) + } else if rawValue == "🚴đŸŊ" { + self.init(baseEmoji: .bicyclist, skinTones: [.medium]) + } else if rawValue == "🚴🏾" { + self.init(baseEmoji: .bicyclist, skinTones: [.mediumDark]) + } else if rawValue == "🚴đŸŋ" { + self.init(baseEmoji: .bicyclist, skinTones: [.dark]) + } else if rawValue == "🚴‍♂ī¸" { + self.init(baseEmoji: .manBiking, skinTones: nil) + } else if rawValue == "🚴đŸģ‍♂ī¸" { + self.init(baseEmoji: .manBiking, skinTones: [.light]) + } else if rawValue == "🚴đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manBiking, skinTones: [.mediumLight]) + } else if rawValue == "🚴đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manBiking, skinTones: [.medium]) + } else if rawValue == "🚴🏾‍♂ī¸" { + self.init(baseEmoji: .manBiking, skinTones: [.mediumDark]) + } else if rawValue == "🚴đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manBiking, skinTones: [.dark]) + } else if rawValue == "🚴‍♀ī¸" { + self.init(baseEmoji: .womanBiking, skinTones: nil) + } else if rawValue == "🚴đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanBiking, skinTones: [.light]) + } else if rawValue == "🚴đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanBiking, skinTones: [.mediumLight]) + } else if rawValue == "🚴đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanBiking, skinTones: [.medium]) + } else if rawValue == "🚴🏾‍♀ī¸" { + self.init(baseEmoji: .womanBiking, skinTones: [.mediumDark]) + } else if rawValue == "🚴đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanBiking, skinTones: [.dark]) + } else if rawValue == "đŸšĩ" { + self.init(baseEmoji: .mountainBicyclist, skinTones: nil) + } else if rawValue == "đŸšĩđŸģ" { + self.init(baseEmoji: .mountainBicyclist, skinTones: [.light]) + } else if rawValue == "đŸšĩđŸŧ" { + self.init(baseEmoji: .mountainBicyclist, skinTones: [.mediumLight]) + } else if rawValue == "đŸšĩđŸŊ" { + self.init(baseEmoji: .mountainBicyclist, skinTones: [.medium]) + } else if rawValue == "đŸšĩ🏾" { + self.init(baseEmoji: .mountainBicyclist, skinTones: [.mediumDark]) + } else if rawValue == "đŸšĩđŸŋ" { + self.init(baseEmoji: .mountainBicyclist, skinTones: [.dark]) + } else if rawValue == "đŸšĩ‍♂ī¸" { + self.init(baseEmoji: .manMountainBiking, skinTones: nil) + } else if rawValue == "đŸšĩđŸģ‍♂ī¸" { + self.init(baseEmoji: .manMountainBiking, skinTones: [.light]) + } else if rawValue == "đŸšĩđŸŧ‍♂ī¸" { + self.init(baseEmoji: .manMountainBiking, skinTones: [.mediumLight]) + } else if rawValue == "đŸšĩđŸŊ‍♂ī¸" { + self.init(baseEmoji: .manMountainBiking, skinTones: [.medium]) + } else if rawValue == "đŸšĩ🏾‍♂ī¸" { + self.init(baseEmoji: .manMountainBiking, skinTones: [.mediumDark]) + } else if rawValue == "đŸšĩđŸŋ‍♂ī¸" { + self.init(baseEmoji: .manMountainBiking, skinTones: [.dark]) + } else if rawValue == "đŸšĩ‍♀ī¸" { + self.init(baseEmoji: .womanMountainBiking, skinTones: nil) + } else if rawValue == "đŸšĩđŸģ‍♀ī¸" { + self.init(baseEmoji: .womanMountainBiking, skinTones: [.light]) + } else if rawValue == "đŸšĩđŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanMountainBiking, skinTones: [.mediumLight]) + } else if rawValue == "đŸšĩđŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanMountainBiking, skinTones: [.medium]) + } else if rawValue == "đŸšĩ🏾‍♀ī¸" { + self.init(baseEmoji: .womanMountainBiking, skinTones: [.mediumDark]) + } else if rawValue == "đŸšĩđŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanMountainBiking, skinTones: [.dark]) + } else if rawValue == "🤸" { + self.init(baseEmoji: .personDoingCartwheel, skinTones: nil) + } else if rawValue == "🤸đŸģ" { + self.init(baseEmoji: .personDoingCartwheel, skinTones: [.light]) + } else if rawValue == "🤸đŸŧ" { + self.init(baseEmoji: .personDoingCartwheel, skinTones: [.mediumLight]) + } else if rawValue == "🤸đŸŊ" { + self.init(baseEmoji: .personDoingCartwheel, skinTones: [.medium]) + } else if rawValue == "🤸🏾" { + self.init(baseEmoji: .personDoingCartwheel, skinTones: [.mediumDark]) + } else if rawValue == "🤸đŸŋ" { + self.init(baseEmoji: .personDoingCartwheel, skinTones: [.dark]) + } else if rawValue == "🤸‍♂ī¸" { + self.init(baseEmoji: .manCartwheeling, skinTones: nil) + } else if rawValue == "🤸đŸģ‍♂ī¸" { + self.init(baseEmoji: .manCartwheeling, skinTones: [.light]) + } else if rawValue == "🤸đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manCartwheeling, skinTones: [.mediumLight]) + } else if rawValue == "🤸đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manCartwheeling, skinTones: [.medium]) + } else if rawValue == "🤸🏾‍♂ī¸" { + self.init(baseEmoji: .manCartwheeling, skinTones: [.mediumDark]) + } else if rawValue == "🤸đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manCartwheeling, skinTones: [.dark]) + } else if rawValue == "🤸‍♀ī¸" { + self.init(baseEmoji: .womanCartwheeling, skinTones: nil) + } else if rawValue == "🤸đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanCartwheeling, skinTones: [.light]) + } else if rawValue == "🤸đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanCartwheeling, skinTones: [.mediumLight]) + } else if rawValue == "🤸đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanCartwheeling, skinTones: [.medium]) + } else if rawValue == "🤸🏾‍♀ī¸" { + self.init(baseEmoji: .womanCartwheeling, skinTones: [.mediumDark]) + } else if rawValue == "🤸đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanCartwheeling, skinTones: [.dark]) + } else if rawValue == "đŸ¤ŧ" { + self.init(baseEmoji: .wrestlers, skinTones: nil) + } else if rawValue == "đŸ¤ŧ‍♂ī¸" { + self.init(baseEmoji: .manWrestling, skinTones: nil) + } else if rawValue == "đŸ¤ŧ‍♀ī¸" { + self.init(baseEmoji: .womanWrestling, skinTones: nil) + } else if rawValue == "đŸ¤Ŋ" { + self.init(baseEmoji: .waterPolo, skinTones: nil) + } else if rawValue == "đŸ¤ŊđŸģ" { + self.init(baseEmoji: .waterPolo, skinTones: [.light]) + } else if rawValue == "đŸ¤ŊđŸŧ" { + self.init(baseEmoji: .waterPolo, skinTones: [.mediumLight]) + } else if rawValue == "đŸ¤ŊđŸŊ" { + self.init(baseEmoji: .waterPolo, skinTones: [.medium]) + } else if rawValue == "đŸ¤Ŋ🏾" { + self.init(baseEmoji: .waterPolo, skinTones: [.mediumDark]) + } else if rawValue == "đŸ¤ŊđŸŋ" { + self.init(baseEmoji: .waterPolo, skinTones: [.dark]) + } else if rawValue == "đŸ¤Ŋ‍♂ī¸" { + self.init(baseEmoji: .manPlayingWaterPolo, skinTones: nil) + } else if rawValue == "đŸ¤ŊđŸģ‍♂ī¸" { + self.init(baseEmoji: .manPlayingWaterPolo, skinTones: [.light]) + } else if rawValue == "đŸ¤ŊđŸŧ‍♂ī¸" { + self.init(baseEmoji: .manPlayingWaterPolo, skinTones: [.mediumLight]) + } else if rawValue == "đŸ¤ŊđŸŊ‍♂ī¸" { + self.init(baseEmoji: .manPlayingWaterPolo, skinTones: [.medium]) + } else if rawValue == "đŸ¤Ŋ🏾‍♂ī¸" { + self.init(baseEmoji: .manPlayingWaterPolo, skinTones: [.mediumDark]) + } else if rawValue == "đŸ¤ŊđŸŋ‍♂ī¸" { + self.init(baseEmoji: .manPlayingWaterPolo, skinTones: [.dark]) + } else if rawValue == "đŸ¤Ŋ‍♀ī¸" { + self.init(baseEmoji: .womanPlayingWaterPolo, skinTones: nil) + } else if rawValue == "đŸ¤ŊđŸģ‍♀ī¸" { + self.init(baseEmoji: .womanPlayingWaterPolo, skinTones: [.light]) + } else if rawValue == "đŸ¤ŊđŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanPlayingWaterPolo, skinTones: [.mediumLight]) + } else if rawValue == "đŸ¤ŊđŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanPlayingWaterPolo, skinTones: [.medium]) + } else if rawValue == "đŸ¤Ŋ🏾‍♀ī¸" { + self.init(baseEmoji: .womanPlayingWaterPolo, skinTones: [.mediumDark]) + } else if rawValue == "đŸ¤ŊđŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanPlayingWaterPolo, skinTones: [.dark]) + } else if rawValue == "🤾" { + self.init(baseEmoji: .handball, skinTones: nil) + } else if rawValue == "🤾đŸģ" { + self.init(baseEmoji: .handball, skinTones: [.light]) + } else if rawValue == "🤾đŸŧ" { + self.init(baseEmoji: .handball, skinTones: [.mediumLight]) + } else if rawValue == "🤾đŸŊ" { + self.init(baseEmoji: .handball, skinTones: [.medium]) + } else if rawValue == "🤾🏾" { + self.init(baseEmoji: .handball, skinTones: [.mediumDark]) + } else if rawValue == "🤾đŸŋ" { + self.init(baseEmoji: .handball, skinTones: [.dark]) + } else if rawValue == "🤾‍♂ī¸" { + self.init(baseEmoji: .manPlayingHandball, skinTones: nil) + } else if rawValue == "🤾đŸģ‍♂ī¸" { + self.init(baseEmoji: .manPlayingHandball, skinTones: [.light]) + } else if rawValue == "🤾đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manPlayingHandball, skinTones: [.mediumLight]) + } else if rawValue == "🤾đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manPlayingHandball, skinTones: [.medium]) + } else if rawValue == "🤾🏾‍♂ī¸" { + self.init(baseEmoji: .manPlayingHandball, skinTones: [.mediumDark]) + } else if rawValue == "🤾đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manPlayingHandball, skinTones: [.dark]) + } else if rawValue == "🤾‍♀ī¸" { + self.init(baseEmoji: .womanPlayingHandball, skinTones: nil) + } else if rawValue == "🤾đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanPlayingHandball, skinTones: [.light]) + } else if rawValue == "🤾đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanPlayingHandball, skinTones: [.mediumLight]) + } else if rawValue == "🤾đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanPlayingHandball, skinTones: [.medium]) + } else if rawValue == "🤾🏾‍♀ī¸" { + self.init(baseEmoji: .womanPlayingHandball, skinTones: [.mediumDark]) + } else if rawValue == "🤾đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanPlayingHandball, skinTones: [.dark]) + } else if rawValue == "🤹" { + self.init(baseEmoji: .juggling, skinTones: nil) + } else if rawValue == "🤹đŸģ" { + self.init(baseEmoji: .juggling, skinTones: [.light]) + } else if rawValue == "🤹đŸŧ" { + self.init(baseEmoji: .juggling, skinTones: [.mediumLight]) + } else if rawValue == "🤹đŸŊ" { + self.init(baseEmoji: .juggling, skinTones: [.medium]) + } else if rawValue == "🤹🏾" { + self.init(baseEmoji: .juggling, skinTones: [.mediumDark]) + } else if rawValue == "🤹đŸŋ" { + self.init(baseEmoji: .juggling, skinTones: [.dark]) + } else if rawValue == "🤹‍♂ī¸" { + self.init(baseEmoji: .manJuggling, skinTones: nil) + } else if rawValue == "🤹đŸģ‍♂ī¸" { + self.init(baseEmoji: .manJuggling, skinTones: [.light]) + } else if rawValue == "🤹đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manJuggling, skinTones: [.mediumLight]) + } else if rawValue == "🤹đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manJuggling, skinTones: [.medium]) + } else if rawValue == "🤹🏾‍♂ī¸" { + self.init(baseEmoji: .manJuggling, skinTones: [.mediumDark]) + } else if rawValue == "🤹đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manJuggling, skinTones: [.dark]) + } else if rawValue == "🤹‍♀ī¸" { + self.init(baseEmoji: .womanJuggling, skinTones: nil) + } else if rawValue == "🤹đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanJuggling, skinTones: [.light]) + } else if rawValue == "🤹đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanJuggling, skinTones: [.mediumLight]) + } else if rawValue == "🤹đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanJuggling, skinTones: [.medium]) + } else if rawValue == "🤹🏾‍♀ī¸" { + self.init(baseEmoji: .womanJuggling, skinTones: [.mediumDark]) + } else if rawValue == "🤹đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanJuggling, skinTones: [.dark]) + } else if rawValue == "🧘" { + self.init(baseEmoji: .personInLotusPosition, skinTones: nil) + } else if rawValue == "🧘đŸģ" { + self.init(baseEmoji: .personInLotusPosition, skinTones: [.light]) + } else if rawValue == "🧘đŸŧ" { + self.init(baseEmoji: .personInLotusPosition, skinTones: [.mediumLight]) + } else if rawValue == "🧘đŸŊ" { + self.init(baseEmoji: .personInLotusPosition, skinTones: [.medium]) + } else if rawValue == "🧘🏾" { + self.init(baseEmoji: .personInLotusPosition, skinTones: [.mediumDark]) + } else if rawValue == "🧘đŸŋ" { + self.init(baseEmoji: .personInLotusPosition, skinTones: [.dark]) + } else if rawValue == "🧘‍♂ī¸" { + self.init(baseEmoji: .manInLotusPosition, skinTones: nil) + } else if rawValue == "🧘đŸģ‍♂ī¸" { + self.init(baseEmoji: .manInLotusPosition, skinTones: [.light]) + } else if rawValue == "🧘đŸŧ‍♂ī¸" { + self.init(baseEmoji: .manInLotusPosition, skinTones: [.mediumLight]) + } else if rawValue == "🧘đŸŊ‍♂ī¸" { + self.init(baseEmoji: .manInLotusPosition, skinTones: [.medium]) + } else if rawValue == "🧘🏾‍♂ī¸" { + self.init(baseEmoji: .manInLotusPosition, skinTones: [.mediumDark]) + } else if rawValue == "🧘đŸŋ‍♂ī¸" { + self.init(baseEmoji: .manInLotusPosition, skinTones: [.dark]) + } else if rawValue == "🧘‍♀ī¸" { + self.init(baseEmoji: .womanInLotusPosition, skinTones: nil) + } else if rawValue == "🧘đŸģ‍♀ī¸" { + self.init(baseEmoji: .womanInLotusPosition, skinTones: [.light]) + } else if rawValue == "🧘đŸŧ‍♀ī¸" { + self.init(baseEmoji: .womanInLotusPosition, skinTones: [.mediumLight]) + } else if rawValue == "🧘đŸŊ‍♀ī¸" { + self.init(baseEmoji: .womanInLotusPosition, skinTones: [.medium]) + } else if rawValue == "🧘🏾‍♀ī¸" { + self.init(baseEmoji: .womanInLotusPosition, skinTones: [.mediumDark]) + } else if rawValue == "🧘đŸŋ‍♀ī¸" { + self.init(baseEmoji: .womanInLotusPosition, skinTones: [.dark]) + } else if rawValue == "🛀" { + self.init(baseEmoji: .bath, skinTones: nil) + } else if rawValue == "🛀đŸģ" { + self.init(baseEmoji: .bath, skinTones: [.light]) + } else if rawValue == "🛀đŸŧ" { + self.init(baseEmoji: .bath, skinTones: [.mediumLight]) + } else if rawValue == "🛀đŸŊ" { + self.init(baseEmoji: .bath, skinTones: [.medium]) + } else if rawValue == "🛀🏾" { + self.init(baseEmoji: .bath, skinTones: [.mediumDark]) + } else if rawValue == "🛀đŸŋ" { + self.init(baseEmoji: .bath, skinTones: [.dark]) + } else if rawValue == "🛌" { + self.init(baseEmoji: .sleepingAccommodation, skinTones: nil) + } else if rawValue == "🛌đŸģ" { + self.init(baseEmoji: .sleepingAccommodation, skinTones: [.light]) + } else if rawValue == "🛌đŸŧ" { + self.init(baseEmoji: .sleepingAccommodation, skinTones: [.mediumLight]) + } else if rawValue == "🛌đŸŊ" { + self.init(baseEmoji: .sleepingAccommodation, skinTones: [.medium]) + } else if rawValue == "🛌🏾" { + self.init(baseEmoji: .sleepingAccommodation, skinTones: [.mediumDark]) + } else if rawValue == "🛌đŸŋ" { + self.init(baseEmoji: .sleepingAccommodation, skinTones: [.dark]) + } else if rawValue == "🧑‍🤝‍🧑" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: nil) + } else if rawValue == "🧑đŸģ‍🤝‍🧑đŸģ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.light]) + } else if rawValue == "🧑đŸģ‍🤝‍🧑đŸŧ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.light, .mediumLight]) + } else if rawValue == "🧑đŸģ‍🤝‍🧑đŸŊ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.light, .medium]) + } else if rawValue == "🧑đŸģ‍🤝‍🧑🏾" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.light, .mediumDark]) + } else if rawValue == "🧑đŸģ‍🤝‍🧑đŸŋ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.light, .dark]) + } else if rawValue == "🧑đŸŧ‍🤝‍🧑đŸŧ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŧ‍🤝‍🧑đŸģ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.mediumLight, .light]) + } else if rawValue == "🧑đŸŧ‍🤝‍🧑đŸŊ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.mediumLight, .medium]) + } else if rawValue == "🧑đŸŧ‍🤝‍🧑🏾" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.mediumLight, .mediumDark]) + } else if rawValue == "🧑đŸŧ‍🤝‍🧑đŸŋ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.mediumLight, .dark]) + } else if rawValue == "🧑đŸŊ‍🤝‍🧑đŸŊ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.medium]) + } else if rawValue == "🧑đŸŊ‍🤝‍🧑đŸģ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.medium, .light]) + } else if rawValue == "🧑đŸŊ‍🤝‍🧑đŸŧ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.medium, .mediumLight]) + } else if rawValue == "🧑đŸŊ‍🤝‍🧑🏾" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.medium, .mediumDark]) + } else if rawValue == "🧑đŸŊ‍🤝‍🧑đŸŋ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.medium, .dark]) + } else if rawValue == "🧑🏾‍🤝‍🧑🏾" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.mediumDark]) + } else if rawValue == "🧑🏾‍🤝‍🧑đŸģ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.mediumDark, .light]) + } else if rawValue == "🧑🏾‍🤝‍🧑đŸŧ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.mediumDark, .mediumLight]) + } else if rawValue == "🧑🏾‍🤝‍🧑đŸŊ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.mediumDark, .medium]) + } else if rawValue == "🧑🏾‍🤝‍🧑đŸŋ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.mediumDark, .dark]) + } else if rawValue == "🧑đŸŋ‍🤝‍🧑đŸŋ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.dark]) + } else if rawValue == "🧑đŸŋ‍🤝‍🧑đŸģ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.dark, .light]) + } else if rawValue == "🧑đŸŋ‍🤝‍🧑đŸŧ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.dark, .mediumLight]) + } else if rawValue == "🧑đŸŋ‍🤝‍🧑đŸŊ" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.dark, .medium]) + } else if rawValue == "🧑đŸŋ‍🤝‍🧑🏾" { + self.init(baseEmoji: .peopleHoldingHands, skinTones: [.dark, .mediumDark]) + } else if rawValue == "👭" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: nil) + } else if rawValue == "👭đŸģ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.light]) + } else if rawValue == "👩đŸģ‍🤝‍👩đŸŧ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.light, .mediumLight]) + } else if rawValue == "👩đŸģ‍🤝‍👩đŸŊ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.light, .medium]) + } else if rawValue == "👩đŸģ‍🤝‍👩🏾" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.light, .mediumDark]) + } else if rawValue == "👩đŸģ‍🤝‍👩đŸŋ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.light, .dark]) + } else if rawValue == "👭đŸŧ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŧ‍🤝‍👩đŸģ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.mediumLight, .light]) + } else if rawValue == "👩đŸŧ‍🤝‍👩đŸŊ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.mediumLight, .medium]) + } else if rawValue == "👩đŸŧ‍🤝‍👩🏾" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.mediumLight, .mediumDark]) + } else if rawValue == "👩đŸŧ‍🤝‍👩đŸŋ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.mediumLight, .dark]) + } else if rawValue == "👭đŸŊ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.medium]) + } else if rawValue == "👩đŸŊ‍🤝‍👩đŸģ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.medium, .light]) + } else if rawValue == "👩đŸŊ‍🤝‍👩đŸŧ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.medium, .mediumLight]) + } else if rawValue == "👩đŸŊ‍🤝‍👩🏾" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.medium, .mediumDark]) + } else if rawValue == "👩đŸŊ‍🤝‍👩đŸŋ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.medium, .dark]) + } else if rawValue == "👭🏾" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.mediumDark]) + } else if rawValue == "👩🏾‍🤝‍👩đŸģ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.mediumDark, .light]) + } else if rawValue == "👩🏾‍🤝‍👩đŸŧ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.mediumDark, .mediumLight]) + } else if rawValue == "👩🏾‍🤝‍👩đŸŊ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.mediumDark, .medium]) + } else if rawValue == "👩🏾‍🤝‍👩đŸŋ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.mediumDark, .dark]) + } else if rawValue == "👭đŸŋ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.dark]) + } else if rawValue == "👩đŸŋ‍🤝‍👩đŸģ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.dark, .light]) + } else if rawValue == "👩đŸŋ‍🤝‍👩đŸŧ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.dark, .mediumLight]) + } else if rawValue == "👩đŸŋ‍🤝‍👩đŸŊ" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.dark, .medium]) + } else if rawValue == "👩đŸŋ‍🤝‍👩🏾" { + self.init(baseEmoji: .twoWomenHoldingHands, skinTones: [.dark, .mediumDark]) + } else if rawValue == "đŸ‘Ģ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: nil) + } else if rawValue == "đŸ‘ĢđŸģ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.light]) + } else if rawValue == "👩đŸģ‍🤝‍👨đŸŧ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.light, .mediumLight]) + } else if rawValue == "👩đŸģ‍🤝‍👨đŸŊ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.light, .medium]) + } else if rawValue == "👩đŸģ‍🤝‍👨🏾" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.light, .mediumDark]) + } else if rawValue == "👩đŸģ‍🤝‍👨đŸŋ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.light, .dark]) + } else if rawValue == "đŸ‘ĢđŸŧ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŧ‍🤝‍👨đŸģ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.mediumLight, .light]) + } else if rawValue == "👩đŸŧ‍🤝‍👨đŸŊ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.mediumLight, .medium]) + } else if rawValue == "👩đŸŧ‍🤝‍👨🏾" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.mediumLight, .mediumDark]) + } else if rawValue == "👩đŸŧ‍🤝‍👨đŸŋ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.mediumLight, .dark]) + } else if rawValue == "đŸ‘ĢđŸŊ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.medium]) + } else if rawValue == "👩đŸŊ‍🤝‍👨đŸģ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.medium, .light]) + } else if rawValue == "👩đŸŊ‍🤝‍👨đŸŧ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.medium, .mediumLight]) + } else if rawValue == "👩đŸŊ‍🤝‍👨🏾" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.medium, .mediumDark]) + } else if rawValue == "👩đŸŊ‍🤝‍👨đŸŋ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.medium, .dark]) + } else if rawValue == "đŸ‘Ģ🏾" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.mediumDark]) + } else if rawValue == "👩🏾‍🤝‍👨đŸģ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.mediumDark, .light]) + } else if rawValue == "👩🏾‍🤝‍👨đŸŧ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.mediumDark, .mediumLight]) + } else if rawValue == "👩🏾‍🤝‍👨đŸŊ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.mediumDark, .medium]) + } else if rawValue == "👩🏾‍🤝‍👨đŸŋ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.mediumDark, .dark]) + } else if rawValue == "đŸ‘ĢđŸŋ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.dark]) + } else if rawValue == "👩đŸŋ‍🤝‍👨đŸģ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.dark, .light]) + } else if rawValue == "👩đŸŋ‍🤝‍👨đŸŧ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.dark, .mediumLight]) + } else if rawValue == "👩đŸŋ‍🤝‍👨đŸŊ" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.dark, .medium]) + } else if rawValue == "👩đŸŋ‍🤝‍👨🏾" { + self.init(baseEmoji: .manAndWomanHoldingHands, skinTones: [.dark, .mediumDark]) + } else if rawValue == "đŸ‘Ŧ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: nil) + } else if rawValue == "đŸ‘ŦđŸģ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.light]) + } else if rawValue == "👨đŸģ‍🤝‍👨đŸŧ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.light, .mediumLight]) + } else if rawValue == "👨đŸģ‍🤝‍👨đŸŊ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.light, .medium]) + } else if rawValue == "👨đŸģ‍🤝‍👨🏾" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.light, .mediumDark]) + } else if rawValue == "👨đŸģ‍🤝‍👨đŸŋ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.light, .dark]) + } else if rawValue == "đŸ‘ŦđŸŧ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŧ‍🤝‍👨đŸģ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.mediumLight, .light]) + } else if rawValue == "👨đŸŧ‍🤝‍👨đŸŊ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.mediumLight, .medium]) + } else if rawValue == "👨đŸŧ‍🤝‍👨🏾" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.mediumLight, .mediumDark]) + } else if rawValue == "👨đŸŧ‍🤝‍👨đŸŋ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.mediumLight, .dark]) + } else if rawValue == "đŸ‘ŦđŸŊ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.medium]) + } else if rawValue == "👨đŸŊ‍🤝‍👨đŸģ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.medium, .light]) + } else if rawValue == "👨đŸŊ‍🤝‍👨đŸŧ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.medium, .mediumLight]) + } else if rawValue == "👨đŸŊ‍🤝‍👨🏾" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.medium, .mediumDark]) + } else if rawValue == "👨đŸŊ‍🤝‍👨đŸŋ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.medium, .dark]) + } else if rawValue == "đŸ‘Ŧ🏾" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.mediumDark]) + } else if rawValue == "👨🏾‍🤝‍👨đŸģ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.mediumDark, .light]) + } else if rawValue == "👨🏾‍🤝‍👨đŸŧ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.mediumDark, .mediumLight]) + } else if rawValue == "👨🏾‍🤝‍👨đŸŊ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.mediumDark, .medium]) + } else if rawValue == "👨🏾‍🤝‍👨đŸŋ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.mediumDark, .dark]) + } else if rawValue == "đŸ‘ŦđŸŋ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.dark]) + } else if rawValue == "👨đŸŋ‍🤝‍👨đŸģ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.dark, .light]) + } else if rawValue == "👨đŸŋ‍🤝‍👨đŸŧ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.dark, .mediumLight]) + } else if rawValue == "👨đŸŋ‍🤝‍👨đŸŊ" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.dark, .medium]) + } else if rawValue == "👨đŸŋ‍🤝‍👨🏾" { + self.init(baseEmoji: .twoMenHoldingHands, skinTones: [.dark, .mediumDark]) + } else if rawValue == "💏" { + self.init(baseEmoji: .personKissPerson, skinTones: nil) + } else if rawValue == "💏đŸģ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.light]) + } else if rawValue == "🧑đŸģ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŧ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.light, .mediumLight]) + } else if rawValue == "🧑đŸģ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŊ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.light, .medium]) + } else if rawValue == "🧑đŸģ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸž" { + self.init(baseEmoji: .personKissPerson, skinTones: [.light, .mediumDark]) + } else if rawValue == "🧑đŸģ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŋ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.light, .dark]) + } else if rawValue == "💏đŸŧ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŧ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸģ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.mediumLight, .light]) + } else if rawValue == "🧑đŸŧ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŊ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.mediumLight, .medium]) + } else if rawValue == "🧑đŸŧ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸž" { + self.init(baseEmoji: .personKissPerson, skinTones: [.mediumLight, .mediumDark]) + } else if rawValue == "🧑đŸŧ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŋ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.mediumLight, .dark]) + } else if rawValue == "💏đŸŊ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.medium]) + } else if rawValue == "🧑đŸŊ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸģ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.medium, .light]) + } else if rawValue == "🧑đŸŊ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŧ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.medium, .mediumLight]) + } else if rawValue == "🧑đŸŊ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸž" { + self.init(baseEmoji: .personKissPerson, skinTones: [.medium, .mediumDark]) + } else if rawValue == "🧑đŸŊ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŋ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.medium, .dark]) + } else if rawValue == "💏🏾" { + self.init(baseEmoji: .personKissPerson, skinTones: [.mediumDark]) + } else if rawValue == "🧑🏾‍❤ī¸â€đŸ’‹â€đŸ§‘đŸģ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.mediumDark, .light]) + } else if rawValue == "🧑🏾‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŧ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.mediumDark, .mediumLight]) + } else if rawValue == "🧑🏾‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŊ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.mediumDark, .medium]) + } else if rawValue == "🧑🏾‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŋ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.mediumDark, .dark]) + } else if rawValue == "💏đŸŋ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.dark]) + } else if rawValue == "🧑đŸŋ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸģ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.dark, .light]) + } else if rawValue == "🧑đŸŋ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŧ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.dark, .mediumLight]) + } else if rawValue == "🧑đŸŋ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸŊ" { + self.init(baseEmoji: .personKissPerson, skinTones: [.dark, .medium]) + } else if rawValue == "🧑đŸŋ‍❤ī¸â€đŸ’‹â€đŸ§‘đŸž" { + self.init(baseEmoji: .personKissPerson, skinTones: [.dark, .mediumDark]) + } else if rawValue == "👩‍❤ī¸â€đŸ’‹â€đŸ‘¨" { + self.init(baseEmoji: .womanKissMan, skinTones: nil) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.light]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.light, .mediumLight]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.light, .medium]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž" { + self.init(baseEmoji: .womanKissMan, skinTones: [.light, .mediumDark]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.light, .dark]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.mediumLight, .light]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.mediumLight, .medium]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž" { + self.init(baseEmoji: .womanKissMan, skinTones: [.mediumLight, .mediumDark]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.mediumLight, .dark]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.medium]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.medium, .light]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.medium, .mediumLight]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž" { + self.init(baseEmoji: .womanKissMan, skinTones: [.medium, .mediumDark]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.medium, .dark]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž" { + self.init(baseEmoji: .womanKissMan, skinTones: [.mediumDark]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.mediumDark, .light]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.mediumDark, .mediumLight]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.mediumDark, .medium]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.mediumDark, .dark]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.dark]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.dark, .light]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.dark, .mediumLight]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .womanKissMan, skinTones: [.dark, .medium]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž" { + self.init(baseEmoji: .womanKissMan, skinTones: [.dark, .mediumDark]) + } else if rawValue == "👨‍❤ī¸â€đŸ’‹â€đŸ‘¨" { + self.init(baseEmoji: .manKissMan, skinTones: nil) + } else if rawValue == "👨đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .manKissMan, skinTones: [.light]) + } else if rawValue == "👨đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .manKissMan, skinTones: [.light, .mediumLight]) + } else if rawValue == "👨đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .manKissMan, skinTones: [.light, .medium]) + } else if rawValue == "👨đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž" { + self.init(baseEmoji: .manKissMan, skinTones: [.light, .mediumDark]) + } else if rawValue == "👨đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .manKissMan, skinTones: [.light, .dark]) + } else if rawValue == "👨đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .manKissMan, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .manKissMan, skinTones: [.mediumLight, .light]) + } else if rawValue == "👨đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .manKissMan, skinTones: [.mediumLight, .medium]) + } else if rawValue == "👨đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž" { + self.init(baseEmoji: .manKissMan, skinTones: [.mediumLight, .mediumDark]) + } else if rawValue == "👨đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .manKissMan, skinTones: [.mediumLight, .dark]) + } else if rawValue == "👨đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .manKissMan, skinTones: [.medium]) + } else if rawValue == "👨đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .manKissMan, skinTones: [.medium, .light]) + } else if rawValue == "👨đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .manKissMan, skinTones: [.medium, .mediumLight]) + } else if rawValue == "👨đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž" { + self.init(baseEmoji: .manKissMan, skinTones: [.medium, .mediumDark]) + } else if rawValue == "👨đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .manKissMan, skinTones: [.medium, .dark]) + } else if rawValue == "👨🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž" { + self.init(baseEmoji: .manKissMan, skinTones: [.mediumDark]) + } else if rawValue == "👨🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .manKissMan, skinTones: [.mediumDark, .light]) + } else if rawValue == "👨🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .manKissMan, skinTones: [.mediumDark, .mediumLight]) + } else if rawValue == "👨🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .manKissMan, skinTones: [.mediumDark, .medium]) + } else if rawValue == "👨🏾‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .manKissMan, skinTones: [.mediumDark, .dark]) + } else if rawValue == "👨đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .manKissMan, skinTones: [.dark]) + } else if rawValue == "👨đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .manKissMan, skinTones: [.dark, .light]) + } else if rawValue == "👨đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .manKissMan, skinTones: [.dark, .mediumLight]) + } else if rawValue == "👨đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .manKissMan, skinTones: [.dark, .medium]) + } else if rawValue == "👨đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘¨đŸž" { + self.init(baseEmoji: .manKissMan, skinTones: [.dark, .mediumDark]) + } else if rawValue == "👩‍❤ī¸â€đŸ’‹â€đŸ‘Š" { + self.init(baseEmoji: .womanKissWoman, skinTones: nil) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸģ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.light]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŧ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.light, .mediumLight]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŊ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.light, .medium]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸž" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.light, .mediumDark]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŋ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.light, .dark]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŧ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸģ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.mediumLight, .light]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŊ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.mediumLight, .medium]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸž" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.mediumLight, .mediumDark]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŋ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.mediumLight, .dark]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŊ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.medium]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸģ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.medium, .light]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŧ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.medium, .mediumLight]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸž" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.medium, .mediumDark]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŋ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.medium, .dark]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸž" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.mediumDark]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸģ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.mediumDark, .light]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŧ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.mediumDark, .mediumLight]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŊ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.mediumDark, .medium]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŋ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.mediumDark, .dark]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŋ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.dark]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸģ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.dark, .light]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŧ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.dark, .mediumLight]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸŊ" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.dark, .medium]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ’‹â€đŸ‘ŠđŸž" { + self.init(baseEmoji: .womanKissWoman, skinTones: [.dark, .mediumDark]) + } else if rawValue == "💑" { + self.init(baseEmoji: .personHeartPerson, skinTones: nil) + } else if rawValue == "💑đŸģ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.light]) + } else if rawValue == "🧑đŸģ‍❤ī¸â€đŸ§‘đŸŧ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.light, .mediumLight]) + } else if rawValue == "🧑đŸģ‍❤ī¸â€đŸ§‘đŸŊ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.light, .medium]) + } else if rawValue == "🧑đŸģ‍❤ī¸â€đŸ§‘đŸž" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.light, .mediumDark]) + } else if rawValue == "🧑đŸģ‍❤ī¸â€đŸ§‘đŸŋ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.light, .dark]) + } else if rawValue == "💑đŸŧ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.mediumLight]) + } else if rawValue == "🧑đŸŧ‍❤ī¸â€đŸ§‘đŸģ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.mediumLight, .light]) + } else if rawValue == "🧑đŸŧ‍❤ī¸â€đŸ§‘đŸŊ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.mediumLight, .medium]) + } else if rawValue == "🧑đŸŧ‍❤ī¸â€đŸ§‘đŸž" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.mediumLight, .mediumDark]) + } else if rawValue == "🧑đŸŧ‍❤ī¸â€đŸ§‘đŸŋ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.mediumLight, .dark]) + } else if rawValue == "💑đŸŊ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.medium]) + } else if rawValue == "🧑đŸŊ‍❤ī¸â€đŸ§‘đŸģ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.medium, .light]) + } else if rawValue == "🧑đŸŊ‍❤ī¸â€đŸ§‘đŸŧ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.medium, .mediumLight]) + } else if rawValue == "🧑đŸŊ‍❤ī¸â€đŸ§‘đŸž" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.medium, .mediumDark]) + } else if rawValue == "🧑đŸŊ‍❤ī¸â€đŸ§‘đŸŋ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.medium, .dark]) + } else if rawValue == "💑🏾" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.mediumDark]) + } else if rawValue == "🧑🏾‍❤ī¸â€đŸ§‘đŸģ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.mediumDark, .light]) + } else if rawValue == "🧑🏾‍❤ī¸â€đŸ§‘đŸŧ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.mediumDark, .mediumLight]) + } else if rawValue == "🧑🏾‍❤ī¸â€đŸ§‘đŸŊ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.mediumDark, .medium]) + } else if rawValue == "🧑🏾‍❤ī¸â€đŸ§‘đŸŋ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.mediumDark, .dark]) + } else if rawValue == "💑đŸŋ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.dark]) + } else if rawValue == "🧑đŸŋ‍❤ī¸â€đŸ§‘đŸģ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.dark, .light]) + } else if rawValue == "🧑đŸŋ‍❤ī¸â€đŸ§‘đŸŧ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.dark, .mediumLight]) + } else if rawValue == "🧑đŸŋ‍❤ī¸â€đŸ§‘đŸŊ" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.dark, .medium]) + } else if rawValue == "🧑đŸŋ‍❤ī¸â€đŸ§‘đŸž" { + self.init(baseEmoji: .personHeartPerson, skinTones: [.dark, .mediumDark]) + } else if rawValue == "👩‍❤ī¸â€đŸ‘¨" { + self.init(baseEmoji: .womanHeartMan, skinTones: nil) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.light]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.light, .mediumLight]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.light, .medium]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ‘¨đŸž" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.light, .mediumDark]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.light, .dark]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.mediumLight, .light]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.mediumLight, .medium]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ‘¨đŸž" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.mediumLight, .mediumDark]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.mediumLight, .dark]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.medium]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.medium, .light]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.medium, .mediumLight]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ‘¨đŸž" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.medium, .mediumDark]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.medium, .dark]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ‘¨đŸž" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.mediumDark]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.mediumDark, .light]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.mediumDark, .mediumLight]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.mediumDark, .medium]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.mediumDark, .dark]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.dark]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.dark, .light]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.dark, .mediumLight]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.dark, .medium]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ‘¨đŸž" { + self.init(baseEmoji: .womanHeartMan, skinTones: [.dark, .mediumDark]) + } else if rawValue == "👨‍❤ī¸â€đŸ‘¨" { + self.init(baseEmoji: .manHeartMan, skinTones: nil) + } else if rawValue == "👨đŸģ‍❤ī¸â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.light]) + } else if rawValue == "👨đŸģ‍❤ī¸â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.light, .mediumLight]) + } else if rawValue == "👨đŸģ‍❤ī¸â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.light, .medium]) + } else if rawValue == "👨đŸģ‍❤ī¸â€đŸ‘¨đŸž" { + self.init(baseEmoji: .manHeartMan, skinTones: [.light, .mediumDark]) + } else if rawValue == "👨đŸģ‍❤ī¸â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.light, .dark]) + } else if rawValue == "👨đŸŧ‍❤ī¸â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.mediumLight]) + } else if rawValue == "👨đŸŧ‍❤ī¸â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.mediumLight, .light]) + } else if rawValue == "👨đŸŧ‍❤ī¸â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.mediumLight, .medium]) + } else if rawValue == "👨đŸŧ‍❤ī¸â€đŸ‘¨đŸž" { + self.init(baseEmoji: .manHeartMan, skinTones: [.mediumLight, .mediumDark]) + } else if rawValue == "👨đŸŧ‍❤ī¸â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.mediumLight, .dark]) + } else if rawValue == "👨đŸŊ‍❤ī¸â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.medium]) + } else if rawValue == "👨đŸŊ‍❤ī¸â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.medium, .light]) + } else if rawValue == "👨đŸŊ‍❤ī¸â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.medium, .mediumLight]) + } else if rawValue == "👨đŸŊ‍❤ī¸â€đŸ‘¨đŸž" { + self.init(baseEmoji: .manHeartMan, skinTones: [.medium, .mediumDark]) + } else if rawValue == "👨đŸŊ‍❤ī¸â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.medium, .dark]) + } else if rawValue == "👨🏾‍❤ī¸â€đŸ‘¨đŸž" { + self.init(baseEmoji: .manHeartMan, skinTones: [.mediumDark]) + } else if rawValue == "👨🏾‍❤ī¸â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.mediumDark, .light]) + } else if rawValue == "👨🏾‍❤ī¸â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.mediumDark, .mediumLight]) + } else if rawValue == "👨🏾‍❤ī¸â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.mediumDark, .medium]) + } else if rawValue == "👨🏾‍❤ī¸â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.mediumDark, .dark]) + } else if rawValue == "👨đŸŋ‍❤ī¸â€đŸ‘¨đŸŋ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.dark]) + } else if rawValue == "👨đŸŋ‍❤ī¸â€đŸ‘¨đŸģ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.dark, .light]) + } else if rawValue == "👨đŸŋ‍❤ī¸â€đŸ‘¨đŸŧ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.dark, .mediumLight]) + } else if rawValue == "👨đŸŋ‍❤ī¸â€đŸ‘¨đŸŊ" { + self.init(baseEmoji: .manHeartMan, skinTones: [.dark, .medium]) + } else if rawValue == "👨đŸŋ‍❤ī¸â€đŸ‘¨đŸž" { + self.init(baseEmoji: .manHeartMan, skinTones: [.dark, .mediumDark]) + } else if rawValue == "👩‍❤ī¸â€đŸ‘Š" { + self.init(baseEmoji: .womanHeartWoman, skinTones: nil) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ‘ŠđŸģ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.light]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ‘ŠđŸŧ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.light, .mediumLight]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ‘ŠđŸŊ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.light, .medium]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ‘ŠđŸž" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.light, .mediumDark]) + } else if rawValue == "👩đŸģ‍❤ī¸â€đŸ‘ŠđŸŋ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.light, .dark]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ‘ŠđŸŧ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.mediumLight]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ‘ŠđŸģ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.mediumLight, .light]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ‘ŠđŸŊ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.mediumLight, .medium]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ‘ŠđŸž" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.mediumLight, .mediumDark]) + } else if rawValue == "👩đŸŧ‍❤ī¸â€đŸ‘ŠđŸŋ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.mediumLight, .dark]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ‘ŠđŸŊ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.medium]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ‘ŠđŸģ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.medium, .light]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ‘ŠđŸŧ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.medium, .mediumLight]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ‘ŠđŸž" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.medium, .mediumDark]) + } else if rawValue == "👩đŸŊ‍❤ī¸â€đŸ‘ŠđŸŋ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.medium, .dark]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ‘ŠđŸž" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.mediumDark]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ‘ŠđŸģ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.mediumDark, .light]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ‘ŠđŸŧ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.mediumDark, .mediumLight]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ‘ŠđŸŊ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.mediumDark, .medium]) + } else if rawValue == "👩🏾‍❤ī¸â€đŸ‘ŠđŸŋ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.mediumDark, .dark]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ‘ŠđŸŋ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.dark]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ‘ŠđŸģ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.dark, .light]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ‘ŠđŸŧ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.dark, .mediumLight]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ‘ŠđŸŊ" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.dark, .medium]) + } else if rawValue == "👩đŸŋ‍❤ī¸â€đŸ‘ŠđŸž" { + self.init(baseEmoji: .womanHeartWoman, skinTones: [.dark, .mediumDark]) + } else if rawValue == "đŸ‘Ē" { + self.init(baseEmoji: .family, skinTones: nil) + } else if rawValue == "👨‍👩‍đŸ‘Ļ" { + self.init(baseEmoji: .manWomanBoy, skinTones: nil) + } else if rawValue == "👨‍👩‍👧" { + self.init(baseEmoji: .manWomanGirl, skinTones: nil) + } else if rawValue == "👨‍👩‍👧‍đŸ‘Ļ" { + self.init(baseEmoji: .manWomanGirlBoy, skinTones: nil) + } else if rawValue == "👨‍👩‍đŸ‘Ļ‍đŸ‘Ļ" { + self.init(baseEmoji: .manWomanBoyBoy, skinTones: nil) + } else if rawValue == "👨‍👩‍👧‍👧" { + self.init(baseEmoji: .manWomanGirlGirl, skinTones: nil) + } else if rawValue == "👨‍👨‍đŸ‘Ļ" { + self.init(baseEmoji: .manManBoy, skinTones: nil) + } else if rawValue == "👨‍👨‍👧" { + self.init(baseEmoji: .manManGirl, skinTones: nil) + } else if rawValue == "👨‍👨‍👧‍đŸ‘Ļ" { + self.init(baseEmoji: .manManGirlBoy, skinTones: nil) + } else if rawValue == "👨‍👨‍đŸ‘Ļ‍đŸ‘Ļ" { + self.init(baseEmoji: .manManBoyBoy, skinTones: nil) + } else if rawValue == "👨‍👨‍👧‍👧" { + self.init(baseEmoji: .manManGirlGirl, skinTones: nil) + } else if rawValue == "👩‍👩‍đŸ‘Ļ" { + self.init(baseEmoji: .womanWomanBoy, skinTones: nil) + } else if rawValue == "👩‍👩‍👧" { + self.init(baseEmoji: .womanWomanGirl, skinTones: nil) + } else if rawValue == "👩‍👩‍👧‍đŸ‘Ļ" { + self.init(baseEmoji: .womanWomanGirlBoy, skinTones: nil) + } else if rawValue == "👩‍👩‍đŸ‘Ļ‍đŸ‘Ļ" { + self.init(baseEmoji: .womanWomanBoyBoy, skinTones: nil) + } else if rawValue == "👩‍👩‍👧‍👧" { + self.init(baseEmoji: .womanWomanGirlGirl, skinTones: nil) + } else if rawValue == "👨‍đŸ‘Ļ" { + self.init(baseEmoji: .manBoy, skinTones: nil) + } else if rawValue == "👨‍đŸ‘Ļ‍đŸ‘Ļ" { + self.init(baseEmoji: .manBoyBoy, skinTones: nil) + } else if rawValue == "👨‍👧" { + self.init(baseEmoji: .manGirl, skinTones: nil) + } else if rawValue == "👨‍👧‍đŸ‘Ļ" { + self.init(baseEmoji: .manGirlBoy, skinTones: nil) + } else if rawValue == "👨‍👧‍👧" { + self.init(baseEmoji: .manGirlGirl, skinTones: nil) + } else if rawValue == "👩‍đŸ‘Ļ" { + self.init(baseEmoji: .womanBoy, skinTones: nil) + } else if rawValue == "👩‍đŸ‘Ļ‍đŸ‘Ļ" { + self.init(baseEmoji: .womanBoyBoy, skinTones: nil) + } else if rawValue == "👩‍👧" { + self.init(baseEmoji: .womanGirl, skinTones: nil) + } else if rawValue == "👩‍👧‍đŸ‘Ļ" { + self.init(baseEmoji: .womanGirlBoy, skinTones: nil) + } else if rawValue == "👩‍👧‍👧" { + self.init(baseEmoji: .womanGirlGirl, skinTones: nil) + } else if rawValue == "đŸ—Ŗī¸" { + self.init(baseEmoji: .speakingHeadInSilhouette, skinTones: nil) + } else if rawValue == "👤" { + self.init(baseEmoji: .bustInSilhouette, skinTones: nil) + } else if rawValue == "đŸ‘Ĩ" { + self.init(baseEmoji: .bustsInSilhouette, skinTones: nil) + } else if rawValue == "đŸĢ‚" { + self.init(baseEmoji: .peopleHugging, skinTones: nil) + } else if rawValue == "đŸ‘Ŗ" { + self.init(baseEmoji: .footprints, skinTones: nil) + } else if rawValue == "đŸģ" { + self.init(baseEmoji: .skinTone2, skinTones: nil) + } else if rawValue == "đŸŧ" { + self.init(baseEmoji: .skinTone3, skinTones: nil) + } else if rawValue == "đŸŊ" { + self.init(baseEmoji: .skinTone4, skinTones: nil) + } else if rawValue == "🏾" { + self.init(baseEmoji: .skinTone5, skinTones: nil) + } else if rawValue == "đŸŋ" { + self.init(baseEmoji: .skinTone6, skinTones: nil) + } else if rawValue == "đŸĩ" { + self.init(baseEmoji: .monkeyFace, skinTones: nil) + } else if rawValue == "🐒" { + self.init(baseEmoji: .monkey, skinTones: nil) + } else if rawValue == "đŸĻ" { + self.init(baseEmoji: .gorilla, skinTones: nil) + } else if rawValue == "đŸĻ§" { + self.init(baseEmoji: .orangutan, skinTones: nil) + } else if rawValue == "đŸļ" { + self.init(baseEmoji: .dog, skinTones: nil) + } else if rawValue == "🐕" { + self.init(baseEmoji: .dog2, skinTones: nil) + } else if rawValue == "đŸĻŽ" { + self.init(baseEmoji: .guideDog, skinTones: nil) + } else if rawValue == "🐕‍đŸĻē" { + self.init(baseEmoji: .serviceDog, skinTones: nil) + } else if rawValue == "🐩" { + self.init(baseEmoji: .poodle, skinTones: nil) + } else if rawValue == "đŸē" { + self.init(baseEmoji: .wolf, skinTones: nil) + } else if rawValue == "đŸĻŠ" { + self.init(baseEmoji: .foxFace, skinTones: nil) + } else if rawValue == "đŸĻ" { + self.init(baseEmoji: .raccoon, skinTones: nil) + } else if rawValue == "🐱" { + self.init(baseEmoji: .cat, skinTones: nil) + } else if rawValue == "🐈" { + self.init(baseEmoji: .cat2, skinTones: nil) + } else if rawValue == "🐈‍âŦ›" { + self.init(baseEmoji: .blackCat, skinTones: nil) + } else if rawValue == "đŸĻ" { + self.init(baseEmoji: .lionFace, skinTones: nil) + } else if rawValue == "đŸ¯" { + self.init(baseEmoji: .tiger, skinTones: nil) + } else if rawValue == "🐅" { + self.init(baseEmoji: .tiger2, skinTones: nil) + } else if rawValue == "🐆" { + self.init(baseEmoji: .leopard, skinTones: nil) + } else if rawValue == "🐴" { + self.init(baseEmoji: .horse, skinTones: nil) + } else if rawValue == "🐎" { + self.init(baseEmoji: .racehorse, skinTones: nil) + } else if rawValue == "đŸĻ„" { + self.init(baseEmoji: .unicornFace, skinTones: nil) + } else if rawValue == "đŸĻ“" { + self.init(baseEmoji: .zebraFace, skinTones: nil) + } else if rawValue == "đŸĻŒ" { + self.init(baseEmoji: .deer, skinTones: nil) + } else if rawValue == "đŸĻŦ" { + self.init(baseEmoji: .bison, skinTones: nil) + } else if rawValue == "🐮" { + self.init(baseEmoji: .cow, skinTones: nil) + } else if rawValue == "🐂" { + self.init(baseEmoji: .ox, skinTones: nil) + } else if rawValue == "🐃" { + self.init(baseEmoji: .waterBuffalo, skinTones: nil) + } else if rawValue == "🐄" { + self.init(baseEmoji: .cow2, skinTones: nil) + } else if rawValue == "🐷" { + self.init(baseEmoji: .pig, skinTones: nil) + } else if rawValue == "🐖" { + self.init(baseEmoji: .pig2, skinTones: nil) + } else if rawValue == "🐗" { + self.init(baseEmoji: .boar, skinTones: nil) + } else if rawValue == "đŸŊ" { + self.init(baseEmoji: .pigNose, skinTones: nil) + } else if rawValue == "🐏" { + self.init(baseEmoji: .ram, skinTones: nil) + } else if rawValue == "🐑" { + self.init(baseEmoji: .sheep, skinTones: nil) + } else if rawValue == "🐐" { + self.init(baseEmoji: .goat, skinTones: nil) + } else if rawValue == "đŸĒ" { + self.init(baseEmoji: .dromedaryCamel, skinTones: nil) + } else if rawValue == "đŸĢ" { + self.init(baseEmoji: .camel, skinTones: nil) + } else if rawValue == "đŸĻ™" { + self.init(baseEmoji: .llama, skinTones: nil) + } else if rawValue == "đŸĻ’" { + self.init(baseEmoji: .giraffeFace, skinTones: nil) + } else if rawValue == "🐘" { + self.init(baseEmoji: .elephant, skinTones: nil) + } else if rawValue == "đŸĻŖ" { + self.init(baseEmoji: .mammoth, skinTones: nil) + } else if rawValue == "đŸĻ" { + self.init(baseEmoji: .rhinoceros, skinTones: nil) + } else if rawValue == "đŸĻ›" { + self.init(baseEmoji: .hippopotamus, skinTones: nil) + } else if rawValue == "🐭" { + self.init(baseEmoji: .mouse, skinTones: nil) + } else if rawValue == "🐁" { + self.init(baseEmoji: .mouse2, skinTones: nil) + } else if rawValue == "🐀" { + self.init(baseEmoji: .rat, skinTones: nil) + } else if rawValue == "🐹" { + self.init(baseEmoji: .hamster, skinTones: nil) + } else if rawValue == "🐰" { + self.init(baseEmoji: .rabbit, skinTones: nil) + } else if rawValue == "🐇" { + self.init(baseEmoji: .rabbit2, skinTones: nil) + } else if rawValue == "đŸŋī¸" { + self.init(baseEmoji: .chipmunk, skinTones: nil) + } else if rawValue == "đŸĻĢ" { + self.init(baseEmoji: .beaver, skinTones: nil) + } else if rawValue == "đŸĻ”" { + self.init(baseEmoji: .hedgehog, skinTones: nil) + } else if rawValue == "đŸĻ‡" { + self.init(baseEmoji: .bat, skinTones: nil) + } else if rawValue == "đŸģ" { + self.init(baseEmoji: .bear, skinTones: nil) + } else if rawValue == "đŸģ‍❄ī¸" { + self.init(baseEmoji: .polarBear, skinTones: nil) + } else if rawValue == "🐨" { + self.init(baseEmoji: .koala, skinTones: nil) + } else if rawValue == "đŸŧ" { + self.init(baseEmoji: .pandaFace, skinTones: nil) + } else if rawValue == "đŸĻĨ" { + self.init(baseEmoji: .sloth, skinTones: nil) + } else if rawValue == "đŸĻĻ" { + self.init(baseEmoji: .otter, skinTones: nil) + } else if rawValue == "đŸĻ¨" { + self.init(baseEmoji: .skunk, skinTones: nil) + } else if rawValue == "đŸĻ˜" { + self.init(baseEmoji: .kangaroo, skinTones: nil) + } else if rawValue == "đŸĻĄ" { + self.init(baseEmoji: .badger, skinTones: nil) + } else if rawValue == "🐾" { + self.init(baseEmoji: .feet, skinTones: nil) + } else if rawValue == "đŸĻƒ" { + self.init(baseEmoji: .turkey, skinTones: nil) + } else if rawValue == "🐔" { + self.init(baseEmoji: .chicken, skinTones: nil) + } else if rawValue == "🐓" { + self.init(baseEmoji: .rooster, skinTones: nil) + } else if rawValue == "đŸŖ" { + self.init(baseEmoji: .hatchingChick, skinTones: nil) + } else if rawValue == "🐤" { + self.init(baseEmoji: .babyChick, skinTones: nil) + } else if rawValue == "đŸĨ" { + self.init(baseEmoji: .hatchedChick, skinTones: nil) + } else if rawValue == "đŸĻ" { + self.init(baseEmoji: .bird, skinTones: nil) + } else if rawValue == "🐧" { + self.init(baseEmoji: .penguin, skinTones: nil) + } else if rawValue == "🕊ī¸" { + self.init(baseEmoji: .doveOfPeace, skinTones: nil) + } else if rawValue == "đŸĻ…" { + self.init(baseEmoji: .eagle, skinTones: nil) + } else if rawValue == "đŸĻ†" { + self.init(baseEmoji: .duck, skinTones: nil) + } else if rawValue == "đŸĻĸ" { + self.init(baseEmoji: .swan, skinTones: nil) + } else if rawValue == "đŸĻ‰" { + self.init(baseEmoji: .owl, skinTones: nil) + } else if rawValue == "đŸĻ¤" { + self.init(baseEmoji: .dodo, skinTones: nil) + } else if rawValue == "đŸĒļ" { + self.init(baseEmoji: .feather, skinTones: nil) + } else if rawValue == "đŸĻŠ" { + self.init(baseEmoji: .flamingo, skinTones: nil) + } else if rawValue == "đŸĻš" { + self.init(baseEmoji: .peacock, skinTones: nil) + } else if rawValue == "đŸĻœ" { + self.init(baseEmoji: .parrot, skinTones: nil) + } else if rawValue == "🐸" { + self.init(baseEmoji: .frog, skinTones: nil) + } else if rawValue == "🐊" { + self.init(baseEmoji: .crocodile, skinTones: nil) + } else if rawValue == "đŸĸ" { + self.init(baseEmoji: .turtle, skinTones: nil) + } else if rawValue == "đŸĻŽ" { + self.init(baseEmoji: .lizard, skinTones: nil) + } else if rawValue == "🐍" { + self.init(baseEmoji: .snake, skinTones: nil) + } else if rawValue == "🐲" { + self.init(baseEmoji: .dragonFace, skinTones: nil) + } else if rawValue == "🐉" { + self.init(baseEmoji: .dragon, skinTones: nil) + } else if rawValue == "đŸĻ•" { + self.init(baseEmoji: .sauropod, skinTones: nil) + } else if rawValue == "đŸĻ–" { + self.init(baseEmoji: .tRex, skinTones: nil) + } else if rawValue == "đŸŗ" { + self.init(baseEmoji: .whale, skinTones: nil) + } else if rawValue == "🐋" { + self.init(baseEmoji: .whale2, skinTones: nil) + } else if rawValue == "đŸŦ" { + self.init(baseEmoji: .dolphin, skinTones: nil) + } else if rawValue == "đŸĻ­" { + self.init(baseEmoji: .seal, skinTones: nil) + } else if rawValue == "🐟" { + self.init(baseEmoji: .fish, skinTones: nil) + } else if rawValue == "🐠" { + self.init(baseEmoji: .tropicalFish, skinTones: nil) + } else if rawValue == "🐡" { + self.init(baseEmoji: .blowfish, skinTones: nil) + } else if rawValue == "đŸĻˆ" { + self.init(baseEmoji: .shark, skinTones: nil) + } else if rawValue == "🐙" { + self.init(baseEmoji: .octopus, skinTones: nil) + } else if rawValue == "🐚" { + self.init(baseEmoji: .shell, skinTones: nil) + } else if rawValue == "đŸĒ¸" { + self.init(baseEmoji: .coral, skinTones: nil) + } else if rawValue == "🐌" { + self.init(baseEmoji: .snail, skinTones: nil) + } else if rawValue == "đŸĻ‹" { + self.init(baseEmoji: .butterfly, skinTones: nil) + } else if rawValue == "🐛" { + self.init(baseEmoji: .bug, skinTones: nil) + } else if rawValue == "🐜" { + self.init(baseEmoji: .ant, skinTones: nil) + } else if rawValue == "🐝" { + self.init(baseEmoji: .bee, skinTones: nil) + } else if rawValue == "đŸĒ˛" { + self.init(baseEmoji: .beetle, skinTones: nil) + } else if rawValue == "🐞" { + self.init(baseEmoji: .ladybug, skinTones: nil) + } else if rawValue == "đŸĻ—" { + self.init(baseEmoji: .cricket, skinTones: nil) + } else if rawValue == "đŸĒŗ" { + self.init(baseEmoji: .cockroach, skinTones: nil) + } else if rawValue == "🕷ī¸" { + self.init(baseEmoji: .spider, skinTones: nil) + } else if rawValue == "🕸ī¸" { + self.init(baseEmoji: .spiderWeb, skinTones: nil) + } else if rawValue == "đŸĻ‚" { + self.init(baseEmoji: .scorpion, skinTones: nil) + } else if rawValue == "đŸĻŸ" { + self.init(baseEmoji: .mosquito, skinTones: nil) + } else if rawValue == "đŸĒ°" { + self.init(baseEmoji: .fly, skinTones: nil) + } else if rawValue == "đŸĒą" { + self.init(baseEmoji: .worm, skinTones: nil) + } else if rawValue == "đŸĻ " { + self.init(baseEmoji: .microbe, skinTones: nil) + } else if rawValue == "💐" { + self.init(baseEmoji: .bouquet, skinTones: nil) + } else if rawValue == "🌸" { + self.init(baseEmoji: .cherryBlossom, skinTones: nil) + } else if rawValue == "💮" { + self.init(baseEmoji: .whiteFlower, skinTones: nil) + } else if rawValue == "đŸĒˇ" { + self.init(baseEmoji: .lotus, skinTones: nil) + } else if rawValue == "đŸĩī¸" { + self.init(baseEmoji: .rosette, skinTones: nil) + } else if rawValue == "🌹" { + self.init(baseEmoji: .rose, skinTones: nil) + } else if rawValue == "đŸĨ€" { + self.init(baseEmoji: .wiltedFlower, skinTones: nil) + } else if rawValue == "đŸŒē" { + self.init(baseEmoji: .hibiscus, skinTones: nil) + } else if rawValue == "đŸŒģ" { + self.init(baseEmoji: .sunflower, skinTones: nil) + } else if rawValue == "đŸŒŧ" { + self.init(baseEmoji: .blossom, skinTones: nil) + } else if rawValue == "🌷" { + self.init(baseEmoji: .tulip, skinTones: nil) + } else if rawValue == "🌱" { + self.init(baseEmoji: .seedling, skinTones: nil) + } else if rawValue == "đŸĒ´" { + self.init(baseEmoji: .pottedPlant, skinTones: nil) + } else if rawValue == "🌲" { + self.init(baseEmoji: .evergreenTree, skinTones: nil) + } else if rawValue == "đŸŒŗ" { + self.init(baseEmoji: .deciduousTree, skinTones: nil) + } else if rawValue == "🌴" { + self.init(baseEmoji: .palmTree, skinTones: nil) + } else if rawValue == "đŸŒĩ" { + self.init(baseEmoji: .cactus, skinTones: nil) + } else if rawValue == "🌾" { + self.init(baseEmoji: .earOfRice, skinTones: nil) + } else if rawValue == "đŸŒŋ" { + self.init(baseEmoji: .herb, skinTones: nil) + } else if rawValue == "☘ī¸" { + self.init(baseEmoji: .shamrock, skinTones: nil) + } else if rawValue == "🍀" { + self.init(baseEmoji: .fourLeafClover, skinTones: nil) + } else if rawValue == "🍁" { + self.init(baseEmoji: .mapleLeaf, skinTones: nil) + } else if rawValue == "🍂" { + self.init(baseEmoji: .fallenLeaf, skinTones: nil) + } else if rawValue == "🍃" { + self.init(baseEmoji: .leaves, skinTones: nil) + } else if rawValue == "đŸĒš" { + self.init(baseEmoji: .emptyNest, skinTones: nil) + } else if rawValue == "đŸĒē" { + self.init(baseEmoji: .nestWithEggs, skinTones: nil) + } else if rawValue == "🍇" { + self.init(baseEmoji: .grapes, skinTones: nil) + } else if rawValue == "🍈" { + self.init(baseEmoji: .melon, skinTones: nil) + } else if rawValue == "🍉" { + self.init(baseEmoji: .watermelon, skinTones: nil) + } else if rawValue == "🍊" { + self.init(baseEmoji: .tangerine, skinTones: nil) + } else if rawValue == "🍋" { + self.init(baseEmoji: .lemon, skinTones: nil) + } else if rawValue == "🍌" { + self.init(baseEmoji: .banana, skinTones: nil) + } else if rawValue == "🍍" { + self.init(baseEmoji: .pineapple, skinTones: nil) + } else if rawValue == "đŸĨ­" { + self.init(baseEmoji: .mango, skinTones: nil) + } else if rawValue == "🍎" { + self.init(baseEmoji: .apple, skinTones: nil) + } else if rawValue == "🍏" { + self.init(baseEmoji: .greenApple, skinTones: nil) + } else if rawValue == "🍐" { + self.init(baseEmoji: .pear, skinTones: nil) + } else if rawValue == "🍑" { + self.init(baseEmoji: .peach, skinTones: nil) + } else if rawValue == "🍒" { + self.init(baseEmoji: .cherries, skinTones: nil) + } else if rawValue == "🍓" { + self.init(baseEmoji: .strawberry, skinTones: nil) + } else if rawValue == "đŸĢ" { + self.init(baseEmoji: .blueberries, skinTones: nil) + } else if rawValue == "đŸĨ" { + self.init(baseEmoji: .kiwifruit, skinTones: nil) + } else if rawValue == "🍅" { + self.init(baseEmoji: .tomato, skinTones: nil) + } else if rawValue == "đŸĢ’" { + self.init(baseEmoji: .olive, skinTones: nil) + } else if rawValue == "đŸĨĨ" { + self.init(baseEmoji: .coconut, skinTones: nil) + } else if rawValue == "đŸĨ‘" { + self.init(baseEmoji: .avocado, skinTones: nil) + } else if rawValue == "🍆" { + self.init(baseEmoji: .eggplant, skinTones: nil) + } else if rawValue == "đŸĨ”" { + self.init(baseEmoji: .potato, skinTones: nil) + } else if rawValue == "đŸĨ•" { + self.init(baseEmoji: .carrot, skinTones: nil) + } else if rawValue == "đŸŒŊ" { + self.init(baseEmoji: .corn, skinTones: nil) + } else if rawValue == "đŸŒļī¸" { + self.init(baseEmoji: .hotPepper, skinTones: nil) + } else if rawValue == "đŸĢ‘" { + self.init(baseEmoji: .bellPepper, skinTones: nil) + } else if rawValue == "đŸĨ’" { + self.init(baseEmoji: .cucumber, skinTones: nil) + } else if rawValue == "đŸĨŦ" { + self.init(baseEmoji: .leafyGreen, skinTones: nil) + } else if rawValue == "đŸĨĻ" { + self.init(baseEmoji: .broccoli, skinTones: nil) + } else if rawValue == "🧄" { + self.init(baseEmoji: .garlic, skinTones: nil) + } else if rawValue == "🧅" { + self.init(baseEmoji: .onion, skinTones: nil) + } else if rawValue == "🍄" { + self.init(baseEmoji: .mushroom, skinTones: nil) + } else if rawValue == "đŸĨœ" { + self.init(baseEmoji: .peanuts, skinTones: nil) + } else if rawValue == "đŸĢ˜" { + self.init(baseEmoji: .beans, skinTones: nil) + } else if rawValue == "🌰" { + self.init(baseEmoji: .chestnut, skinTones: nil) + } else if rawValue == "🍞" { + self.init(baseEmoji: .bread, skinTones: nil) + } else if rawValue == "đŸĨ" { + self.init(baseEmoji: .croissant, skinTones: nil) + } else if rawValue == "đŸĨ–" { + self.init(baseEmoji: .baguetteBread, skinTones: nil) + } else if rawValue == "đŸĢ“" { + self.init(baseEmoji: .flatbread, skinTones: nil) + } else if rawValue == "đŸĨ¨" { + self.init(baseEmoji: .pretzel, skinTones: nil) + } else if rawValue == "đŸĨ¯" { + self.init(baseEmoji: .bagel, skinTones: nil) + } else if rawValue == "đŸĨž" { + self.init(baseEmoji: .pancakes, skinTones: nil) + } else if rawValue == "🧇" { + self.init(baseEmoji: .waffle, skinTones: nil) + } else if rawValue == "🧀" { + self.init(baseEmoji: .cheeseWedge, skinTones: nil) + } else if rawValue == "🍖" { + self.init(baseEmoji: .meatOnBone, skinTones: nil) + } else if rawValue == "🍗" { + self.init(baseEmoji: .poultryLeg, skinTones: nil) + } else if rawValue == "đŸĨŠ" { + self.init(baseEmoji: .cutOfMeat, skinTones: nil) + } else if rawValue == "đŸĨ“" { + self.init(baseEmoji: .bacon, skinTones: nil) + } else if rawValue == "🍔" { + self.init(baseEmoji: .hamburger, skinTones: nil) + } else if rawValue == "🍟" { + self.init(baseEmoji: .fries, skinTones: nil) + } else if rawValue == "🍕" { + self.init(baseEmoji: .pizza, skinTones: nil) + } else if rawValue == "🌭" { + self.init(baseEmoji: .hotdog, skinTones: nil) + } else if rawValue == "đŸĨĒ" { + self.init(baseEmoji: .sandwich, skinTones: nil) + } else if rawValue == "🌮" { + self.init(baseEmoji: .taco, skinTones: nil) + } else if rawValue == "đŸŒ¯" { + self.init(baseEmoji: .burrito, skinTones: nil) + } else if rawValue == "đŸĢ”" { + self.init(baseEmoji: .tamale, skinTones: nil) + } else if rawValue == "đŸĨ™" { + self.init(baseEmoji: .stuffedFlatbread, skinTones: nil) + } else if rawValue == "🧆" { + self.init(baseEmoji: .falafel, skinTones: nil) + } else if rawValue == "đŸĨš" { + self.init(baseEmoji: .egg, skinTones: nil) + } else if rawValue == "đŸŗ" { + self.init(baseEmoji: .friedEgg, skinTones: nil) + } else if rawValue == "đŸĨ˜" { + self.init(baseEmoji: .shallowPanOfFood, skinTones: nil) + } else if rawValue == "🍲" { + self.init(baseEmoji: .stew, skinTones: nil) + } else if rawValue == "đŸĢ•" { + self.init(baseEmoji: .fondue, skinTones: nil) + } else if rawValue == "đŸĨŖ" { + self.init(baseEmoji: .bowlWithSpoon, skinTones: nil) + } else if rawValue == "đŸĨ—" { + self.init(baseEmoji: .greenSalad, skinTones: nil) + } else if rawValue == "đŸŋ" { + self.init(baseEmoji: .popcorn, skinTones: nil) + } else if rawValue == "🧈" { + self.init(baseEmoji: .butter, skinTones: nil) + } else if rawValue == "🧂" { + self.init(baseEmoji: .salt, skinTones: nil) + } else if rawValue == "đŸĨĢ" { + self.init(baseEmoji: .cannedFood, skinTones: nil) + } else if rawValue == "🍱" { + self.init(baseEmoji: .bento, skinTones: nil) + } else if rawValue == "🍘" { + self.init(baseEmoji: .riceCracker, skinTones: nil) + } else if rawValue == "🍙" { + self.init(baseEmoji: .riceBall, skinTones: nil) + } else if rawValue == "🍚" { + self.init(baseEmoji: .rice, skinTones: nil) + } else if rawValue == "🍛" { + self.init(baseEmoji: .curry, skinTones: nil) + } else if rawValue == "🍜" { + self.init(baseEmoji: .ramen, skinTones: nil) + } else if rawValue == "🍝" { + self.init(baseEmoji: .spaghetti, skinTones: nil) + } else if rawValue == "🍠" { + self.init(baseEmoji: .sweetPotato, skinTones: nil) + } else if rawValue == "đŸĸ" { + self.init(baseEmoji: .oden, skinTones: nil) + } else if rawValue == "đŸŖ" { + self.init(baseEmoji: .sushi, skinTones: nil) + } else if rawValue == "🍤" { + self.init(baseEmoji: .friedShrimp, skinTones: nil) + } else if rawValue == "đŸĨ" { + self.init(baseEmoji: .fishCake, skinTones: nil) + } else if rawValue == "đŸĨŽ" { + self.init(baseEmoji: .moonCake, skinTones: nil) + } else if rawValue == "🍡" { + self.init(baseEmoji: .dango, skinTones: nil) + } else if rawValue == "đŸĨŸ" { + self.init(baseEmoji: .dumpling, skinTones: nil) + } else if rawValue == "đŸĨ " { + self.init(baseEmoji: .fortuneCookie, skinTones: nil) + } else if rawValue == "đŸĨĄ" { + self.init(baseEmoji: .takeoutBox, skinTones: nil) + } else if rawValue == "đŸĻ€" { + self.init(baseEmoji: .crab, skinTones: nil) + } else if rawValue == "đŸĻž" { + self.init(baseEmoji: .lobster, skinTones: nil) + } else if rawValue == "đŸĻ" { + self.init(baseEmoji: .shrimp, skinTones: nil) + } else if rawValue == "đŸĻ‘" { + self.init(baseEmoji: .squid, skinTones: nil) + } else if rawValue == "đŸĻĒ" { + self.init(baseEmoji: .oyster, skinTones: nil) + } else if rawValue == "đŸĻ" { + self.init(baseEmoji: .icecream, skinTones: nil) + } else if rawValue == "🍧" { + self.init(baseEmoji: .shavedIce, skinTones: nil) + } else if rawValue == "🍨" { + self.init(baseEmoji: .iceCream, skinTones: nil) + } else if rawValue == "🍩" { + self.init(baseEmoji: .doughnut, skinTones: nil) + } else if rawValue == "đŸĒ" { + self.init(baseEmoji: .cookie, skinTones: nil) + } else if rawValue == "🎂" { + self.init(baseEmoji: .birthday, skinTones: nil) + } else if rawValue == "🍰" { + self.init(baseEmoji: .cake, skinTones: nil) + } else if rawValue == "🧁" { + self.init(baseEmoji: .cupcake, skinTones: nil) + } else if rawValue == "đŸĨ§" { + self.init(baseEmoji: .pie, skinTones: nil) + } else if rawValue == "đŸĢ" { + self.init(baseEmoji: .chocolateBar, skinTones: nil) + } else if rawValue == "đŸŦ" { + self.init(baseEmoji: .candy, skinTones: nil) + } else if rawValue == "🍭" { + self.init(baseEmoji: .lollipop, skinTones: nil) + } else if rawValue == "🍮" { + self.init(baseEmoji: .custard, skinTones: nil) + } else if rawValue == "đŸ¯" { + self.init(baseEmoji: .honeyPot, skinTones: nil) + } else if rawValue == "đŸŧ" { + self.init(baseEmoji: .babyBottle, skinTones: nil) + } else if rawValue == "đŸĨ›" { + self.init(baseEmoji: .glassOfMilk, skinTones: nil) + } else if rawValue == "☕" { + self.init(baseEmoji: .coffee, skinTones: nil) + } else if rawValue == "đŸĢ–" { + self.init(baseEmoji: .teapot, skinTones: nil) + } else if rawValue == "đŸĩ" { + self.init(baseEmoji: .tea, skinTones: nil) + } else if rawValue == "đŸļ" { + self.init(baseEmoji: .sake, skinTones: nil) + } else if rawValue == "🍾" { + self.init(baseEmoji: .champagne, skinTones: nil) + } else if rawValue == "🍷" { + self.init(baseEmoji: .wineGlass, skinTones: nil) + } else if rawValue == "🍸" { + self.init(baseEmoji: .cocktail, skinTones: nil) + } else if rawValue == "🍹" { + self.init(baseEmoji: .tropicalDrink, skinTones: nil) + } else if rawValue == "đŸē" { + self.init(baseEmoji: .beer, skinTones: nil) + } else if rawValue == "đŸģ" { + self.init(baseEmoji: .beers, skinTones: nil) + } else if rawValue == "đŸĨ‚" { + self.init(baseEmoji: .clinkingGlasses, skinTones: nil) + } else if rawValue == "đŸĨƒ" { + self.init(baseEmoji: .tumblerGlass, skinTones: nil) + } else if rawValue == "đŸĢ—" { + self.init(baseEmoji: .pouringLiquid, skinTones: nil) + } else if rawValue == "đŸĨ¤" { + self.init(baseEmoji: .cupWithStraw, skinTones: nil) + } else if rawValue == "🧋" { + self.init(baseEmoji: .bubbleTea, skinTones: nil) + } else if rawValue == "🧃" { + self.init(baseEmoji: .beverageBox, skinTones: nil) + } else if rawValue == "🧉" { + self.init(baseEmoji: .mateDrink, skinTones: nil) + } else if rawValue == "🧊" { + self.init(baseEmoji: .iceCube, skinTones: nil) + } else if rawValue == "đŸĨĸ" { + self.init(baseEmoji: .chopsticks, skinTones: nil) + } else if rawValue == "đŸŊī¸" { + self.init(baseEmoji: .knifeForkPlate, skinTones: nil) + } else if rawValue == "🍴" { + self.init(baseEmoji: .forkAndKnife, skinTones: nil) + } else if rawValue == "đŸĨ„" { + self.init(baseEmoji: .spoon, skinTones: nil) + } else if rawValue == "đŸ”Ē" { + self.init(baseEmoji: .hocho, skinTones: nil) + } else if rawValue == "đŸĢ™" { + self.init(baseEmoji: .jar, skinTones: nil) + } else if rawValue == "đŸē" { + self.init(baseEmoji: .amphora, skinTones: nil) + } else if rawValue == "🌍" { + self.init(baseEmoji: .earthAfrica, skinTones: nil) + } else if rawValue == "🌎" { + self.init(baseEmoji: .earthAmericas, skinTones: nil) + } else if rawValue == "🌏" { + self.init(baseEmoji: .earthAsia, skinTones: nil) + } else if rawValue == "🌐" { + self.init(baseEmoji: .globeWithMeridians, skinTones: nil) + } else if rawValue == "đŸ—ēī¸" { + self.init(baseEmoji: .worldMap, skinTones: nil) + } else if rawValue == "🗾" { + self.init(baseEmoji: .japan, skinTones: nil) + } else if rawValue == "🧭" { + self.init(baseEmoji: .compass, skinTones: nil) + } else if rawValue == "🏔ī¸" { + self.init(baseEmoji: .snowCappedMountain, skinTones: nil) + } else if rawValue == "⛰ī¸" { + self.init(baseEmoji: .mountain, skinTones: nil) + } else if rawValue == "🌋" { + self.init(baseEmoji: .volcano, skinTones: nil) + } else if rawValue == "đŸ—ģ" { + self.init(baseEmoji: .mountFuji, skinTones: nil) + } else if rawValue == "🏕ī¸" { + self.init(baseEmoji: .camping, skinTones: nil) + } else if rawValue == "🏖ī¸" { + self.init(baseEmoji: .beachWithUmbrella, skinTones: nil) + } else if rawValue == "🏜ī¸" { + self.init(baseEmoji: .desert, skinTones: nil) + } else if rawValue == "🏝ī¸" { + self.init(baseEmoji: .desertIsland, skinTones: nil) + } else if rawValue == "🏞ī¸" { + self.init(baseEmoji: .nationalPark, skinTones: nil) + } else if rawValue == "🏟ī¸" { + self.init(baseEmoji: .stadium, skinTones: nil) + } else if rawValue == "🏛ī¸" { + self.init(baseEmoji: .classicalBuilding, skinTones: nil) + } else if rawValue == "🏗ī¸" { + self.init(baseEmoji: .buildingConstruction, skinTones: nil) + } else if rawValue == "🧱" { + self.init(baseEmoji: .bricks, skinTones: nil) + } else if rawValue == "đŸĒ¨" { + self.init(baseEmoji: .rock, skinTones: nil) + } else if rawValue == "đŸĒĩ" { + self.init(baseEmoji: .wood, skinTones: nil) + } else if rawValue == "🛖" { + self.init(baseEmoji: .hut, skinTones: nil) + } else if rawValue == "🏘ī¸" { + self.init(baseEmoji: .houseBuildings, skinTones: nil) + } else if rawValue == "🏚ī¸" { + self.init(baseEmoji: .derelictHouseBuilding, skinTones: nil) + } else if rawValue == "🏠" { + self.init(baseEmoji: .house, skinTones: nil) + } else if rawValue == "🏡" { + self.init(baseEmoji: .houseWithGarden, skinTones: nil) + } else if rawValue == "đŸĸ" { + self.init(baseEmoji: .office, skinTones: nil) + } else if rawValue == "đŸŖ" { + self.init(baseEmoji: .postOffice, skinTones: nil) + } else if rawValue == "🏤" { + self.init(baseEmoji: .europeanPostOffice, skinTones: nil) + } else if rawValue == "đŸĨ" { + self.init(baseEmoji: .hospital, skinTones: nil) + } else if rawValue == "đŸĻ" { + self.init(baseEmoji: .bank, skinTones: nil) + } else if rawValue == "🏨" { + self.init(baseEmoji: .hotel, skinTones: nil) + } else if rawValue == "🏩" { + self.init(baseEmoji: .loveHotel, skinTones: nil) + } else if rawValue == "đŸĒ" { + self.init(baseEmoji: .convenienceStore, skinTones: nil) + } else if rawValue == "đŸĢ" { + self.init(baseEmoji: .school, skinTones: nil) + } else if rawValue == "đŸŦ" { + self.init(baseEmoji: .departmentStore, skinTones: nil) + } else if rawValue == "🏭" { + self.init(baseEmoji: .factory, skinTones: nil) + } else if rawValue == "đŸ¯" { + self.init(baseEmoji: .japaneseCastle, skinTones: nil) + } else if rawValue == "🏰" { + self.init(baseEmoji: .europeanCastle, skinTones: nil) + } else if rawValue == "💒" { + self.init(baseEmoji: .wedding, skinTones: nil) + } else if rawValue == "đŸ—ŧ" { + self.init(baseEmoji: .tokyoTower, skinTones: nil) + } else if rawValue == "đŸ—Ŋ" { + self.init(baseEmoji: .statueOfLiberty, skinTones: nil) + } else if rawValue == "â›Ē" { + self.init(baseEmoji: .church, skinTones: nil) + } else if rawValue == "🕌" { + self.init(baseEmoji: .mosque, skinTones: nil) + } else if rawValue == "🛕" { + self.init(baseEmoji: .hinduTemple, skinTones: nil) + } else if rawValue == "🕍" { + self.init(baseEmoji: .synagogue, skinTones: nil) + } else if rawValue == "⛩ī¸" { + self.init(baseEmoji: .shintoShrine, skinTones: nil) + } else if rawValue == "🕋" { + self.init(baseEmoji: .kaaba, skinTones: nil) + } else if rawValue == "⛲" { + self.init(baseEmoji: .fountain, skinTones: nil) + } else if rawValue == "â›ē" { + self.init(baseEmoji: .tent, skinTones: nil) + } else if rawValue == "🌁" { + self.init(baseEmoji: .foggy, skinTones: nil) + } else if rawValue == "🌃" { + self.init(baseEmoji: .nightWithStars, skinTones: nil) + } else if rawValue == "🏙ī¸" { + self.init(baseEmoji: .cityscape, skinTones: nil) + } else if rawValue == "🌄" { + self.init(baseEmoji: .sunriseOverMountains, skinTones: nil) + } else if rawValue == "🌅" { + self.init(baseEmoji: .sunrise, skinTones: nil) + } else if rawValue == "🌆" { + self.init(baseEmoji: .citySunset, skinTones: nil) + } else if rawValue == "🌇" { + self.init(baseEmoji: .citySunrise, skinTones: nil) + } else if rawValue == "🌉" { + self.init(baseEmoji: .bridgeAtNight, skinTones: nil) + } else if rawValue == "♨ī¸" { + self.init(baseEmoji: .hotsprings, skinTones: nil) + } else if rawValue == "🎠" { + self.init(baseEmoji: .carouselHorse, skinTones: nil) + } else if rawValue == "🛝" { + self.init(baseEmoji: .playgroundSlide, skinTones: nil) + } else if rawValue == "🎡" { + self.init(baseEmoji: .ferrisWheel, skinTones: nil) + } else if rawValue == "đŸŽĸ" { + self.init(baseEmoji: .rollerCoaster, skinTones: nil) + } else if rawValue == "💈" { + self.init(baseEmoji: .barber, skinTones: nil) + } else if rawValue == "đŸŽĒ" { + self.init(baseEmoji: .circusTent, skinTones: nil) + } else if rawValue == "🚂" { + self.init(baseEmoji: .steamLocomotive, skinTones: nil) + } else if rawValue == "🚃" { + self.init(baseEmoji: .railwayCar, skinTones: nil) + } else if rawValue == "🚄" { + self.init(baseEmoji: .bullettrainSide, skinTones: nil) + } else if rawValue == "🚅" { + self.init(baseEmoji: .bullettrainFront, skinTones: nil) + } else if rawValue == "🚆" { + self.init(baseEmoji: .train2, skinTones: nil) + } else if rawValue == "🚇" { + self.init(baseEmoji: .metro, skinTones: nil) + } else if rawValue == "🚈" { + self.init(baseEmoji: .lightRail, skinTones: nil) + } else if rawValue == "🚉" { + self.init(baseEmoji: .station, skinTones: nil) + } else if rawValue == "🚊" { + self.init(baseEmoji: .tram, skinTones: nil) + } else if rawValue == "🚝" { + self.init(baseEmoji: .monorail, skinTones: nil) + } else if rawValue == "🚞" { + self.init(baseEmoji: .mountainRailway, skinTones: nil) + } else if rawValue == "🚋" { + self.init(baseEmoji: .train, skinTones: nil) + } else if rawValue == "🚌" { + self.init(baseEmoji: .bus, skinTones: nil) + } else if rawValue == "🚍" { + self.init(baseEmoji: .oncomingBus, skinTones: nil) + } else if rawValue == "🚎" { + self.init(baseEmoji: .trolleybus, skinTones: nil) + } else if rawValue == "🚐" { + self.init(baseEmoji: .minibus, skinTones: nil) + } else if rawValue == "🚑" { + self.init(baseEmoji: .ambulance, skinTones: nil) + } else if rawValue == "🚒" { + self.init(baseEmoji: .fireEngine, skinTones: nil) + } else if rawValue == "🚓" { + self.init(baseEmoji: .policeCar, skinTones: nil) + } else if rawValue == "🚔" { + self.init(baseEmoji: .oncomingPoliceCar, skinTones: nil) + } else if rawValue == "🚕" { + self.init(baseEmoji: .taxi, skinTones: nil) + } else if rawValue == "🚖" { + self.init(baseEmoji: .oncomingTaxi, skinTones: nil) + } else if rawValue == "🚗" { + self.init(baseEmoji: .car, skinTones: nil) + } else if rawValue == "🚘" { + self.init(baseEmoji: .oncomingAutomobile, skinTones: nil) + } else if rawValue == "🚙" { + self.init(baseEmoji: .blueCar, skinTones: nil) + } else if rawValue == "đŸ›ģ" { + self.init(baseEmoji: .pickupTruck, skinTones: nil) + } else if rawValue == "🚚" { + self.init(baseEmoji: .truck, skinTones: nil) + } else if rawValue == "🚛" { + self.init(baseEmoji: .articulatedLorry, skinTones: nil) + } else if rawValue == "🚜" { + self.init(baseEmoji: .tractor, skinTones: nil) + } else if rawValue == "🏎ī¸" { + self.init(baseEmoji: .racingCar, skinTones: nil) + } else if rawValue == "🏍ī¸" { + self.init(baseEmoji: .racingMotorcycle, skinTones: nil) + } else if rawValue == "đŸ›ĩ" { + self.init(baseEmoji: .motorScooter, skinTones: nil) + } else if rawValue == "đŸĻŊ" { + self.init(baseEmoji: .manualWheelchair, skinTones: nil) + } else if rawValue == "đŸĻŧ" { + self.init(baseEmoji: .motorizedWheelchair, skinTones: nil) + } else if rawValue == "đŸ›ē" { + self.init(baseEmoji: .autoRickshaw, skinTones: nil) + } else if rawValue == "🚲" { + self.init(baseEmoji: .bike, skinTones: nil) + } else if rawValue == "🛴" { + self.init(baseEmoji: .scooter, skinTones: nil) + } else if rawValue == "🛹" { + self.init(baseEmoji: .skateboard, skinTones: nil) + } else if rawValue == "đŸ›ŧ" { + self.init(baseEmoji: .rollerSkate, skinTones: nil) + } else if rawValue == "🚏" { + self.init(baseEmoji: .busstop, skinTones: nil) + } else if rawValue == "đŸ›Ŗī¸" { + self.init(baseEmoji: .motorway, skinTones: nil) + } else if rawValue == "🛤ī¸" { + self.init(baseEmoji: .railwayTrack, skinTones: nil) + } else if rawValue == "đŸ›ĸī¸" { + self.init(baseEmoji: .oilDrum, skinTones: nil) + } else if rawValue == "â›Ŋ" { + self.init(baseEmoji: .fuelpump, skinTones: nil) + } else if rawValue == "🛞" { + self.init(baseEmoji: .wheel, skinTones: nil) + } else if rawValue == "🚨" { + self.init(baseEmoji: .rotatingLight, skinTones: nil) + } else if rawValue == "đŸšĨ" { + self.init(baseEmoji: .trafficLight, skinTones: nil) + } else if rawValue == "đŸšĻ" { + self.init(baseEmoji: .verticalTrafficLight, skinTones: nil) + } else if rawValue == "🛑" { + self.init(baseEmoji: .octagonalSign, skinTones: nil) + } else if rawValue == "🚧" { + self.init(baseEmoji: .construction, skinTones: nil) + } else if rawValue == "⚓" { + self.init(baseEmoji: .anchor, skinTones: nil) + } else if rawValue == "🛟" { + self.init(baseEmoji: .ringBuoy, skinTones: nil) + } else if rawValue == "â›ĩ" { + self.init(baseEmoji: .boat, skinTones: nil) + } else if rawValue == "đŸ›ļ" { + self.init(baseEmoji: .canoe, skinTones: nil) + } else if rawValue == "🚤" { + self.init(baseEmoji: .speedboat, skinTones: nil) + } else if rawValue == "đŸ›ŗī¸" { + self.init(baseEmoji: .passengerShip, skinTones: nil) + } else if rawValue == "⛴ī¸" { + self.init(baseEmoji: .ferry, skinTones: nil) + } else if rawValue == "đŸ›Ĩī¸" { + self.init(baseEmoji: .motorBoat, skinTones: nil) + } else if rawValue == "đŸšĸ" { + self.init(baseEmoji: .ship, skinTones: nil) + } else if rawValue == "✈ī¸" { + self.init(baseEmoji: .airplane, skinTones: nil) + } else if rawValue == "🛩ī¸" { + self.init(baseEmoji: .smallAirplane, skinTones: nil) + } else if rawValue == "đŸ›Ģ" { + self.init(baseEmoji: .airplaneDeparture, skinTones: nil) + } else if rawValue == "đŸ›Ŧ" { + self.init(baseEmoji: .airplaneArriving, skinTones: nil) + } else if rawValue == "đŸĒ‚" { + self.init(baseEmoji: .parachute, skinTones: nil) + } else if rawValue == "đŸ’ē" { + self.init(baseEmoji: .seat, skinTones: nil) + } else if rawValue == "🚁" { + self.init(baseEmoji: .helicopter, skinTones: nil) + } else if rawValue == "🚟" { + self.init(baseEmoji: .suspensionRailway, skinTones: nil) + } else if rawValue == "🚠" { + self.init(baseEmoji: .mountainCableway, skinTones: nil) + } else if rawValue == "🚡" { + self.init(baseEmoji: .aerialTramway, skinTones: nil) + } else if rawValue == "🛰ī¸" { + self.init(baseEmoji: .satellite, skinTones: nil) + } else if rawValue == "🚀" { + self.init(baseEmoji: .rocket, skinTones: nil) + } else if rawValue == "🛸" { + self.init(baseEmoji: .flyingSaucer, skinTones: nil) + } else if rawValue == "🛎ī¸" { + self.init(baseEmoji: .bellhopBell, skinTones: nil) + } else if rawValue == "đŸ§ŗ" { + self.init(baseEmoji: .luggage, skinTones: nil) + } else if rawValue == "⌛" { + self.init(baseEmoji: .hourglass, skinTones: nil) + } else if rawValue == "âŗ" { + self.init(baseEmoji: .hourglassFlowingSand, skinTones: nil) + } else if rawValue == "⌚" { + self.init(baseEmoji: .watch, skinTones: nil) + } else if rawValue == "⏰" { + self.init(baseEmoji: .alarmClock, skinTones: nil) + } else if rawValue == "⏱ī¸" { + self.init(baseEmoji: .stopwatch, skinTones: nil) + } else if rawValue == "⏲ī¸" { + self.init(baseEmoji: .timerClock, skinTones: nil) + } else if rawValue == "🕰ī¸" { + self.init(baseEmoji: .mantelpieceClock, skinTones: nil) + } else if rawValue == "🕛" { + self.init(baseEmoji: .clock12, skinTones: nil) + } else if rawValue == "🕧" { + self.init(baseEmoji: .clock1230, skinTones: nil) + } else if rawValue == "🕐" { + self.init(baseEmoji: .clock1, skinTones: nil) + } else if rawValue == "🕜" { + self.init(baseEmoji: .clock130, skinTones: nil) + } else if rawValue == "🕑" { + self.init(baseEmoji: .clock2, skinTones: nil) + } else if rawValue == "🕝" { + self.init(baseEmoji: .clock230, skinTones: nil) + } else if rawValue == "🕒" { + self.init(baseEmoji: .clock3, skinTones: nil) + } else if rawValue == "🕞" { + self.init(baseEmoji: .clock330, skinTones: nil) + } else if rawValue == "🕓" { + self.init(baseEmoji: .clock4, skinTones: nil) + } else if rawValue == "🕟" { + self.init(baseEmoji: .clock430, skinTones: nil) + } else if rawValue == "🕔" { + self.init(baseEmoji: .clock5, skinTones: nil) + } else if rawValue == "🕠" { + self.init(baseEmoji: .clock530, skinTones: nil) + } else if rawValue == "🕕" { + self.init(baseEmoji: .clock6, skinTones: nil) + } else if rawValue == "🕡" { + self.init(baseEmoji: .clock630, skinTones: nil) + } else if rawValue == "🕖" { + self.init(baseEmoji: .clock7, skinTones: nil) + } else if rawValue == "đŸ•ĸ" { + self.init(baseEmoji: .clock730, skinTones: nil) + } else if rawValue == "🕗" { + self.init(baseEmoji: .clock8, skinTones: nil) + } else if rawValue == "đŸ•Ŗ" { + self.init(baseEmoji: .clock830, skinTones: nil) + } else if rawValue == "🕘" { + self.init(baseEmoji: .clock9, skinTones: nil) + } else if rawValue == "🕤" { + self.init(baseEmoji: .clock930, skinTones: nil) + } else if rawValue == "🕙" { + self.init(baseEmoji: .clock10, skinTones: nil) + } else if rawValue == "đŸ•Ĩ" { + self.init(baseEmoji: .clock1030, skinTones: nil) + } else if rawValue == "🕚" { + self.init(baseEmoji: .clock11, skinTones: nil) + } else if rawValue == "đŸ•Ļ" { + self.init(baseEmoji: .clock1130, skinTones: nil) + } else if rawValue == "🌑" { + self.init(baseEmoji: .newMoon, skinTones: nil) + } else if rawValue == "🌒" { + self.init(baseEmoji: .waxingCrescentMoon, skinTones: nil) + } else if rawValue == "🌓" { + self.init(baseEmoji: .firstQuarterMoon, skinTones: nil) + } else if rawValue == "🌔" { + self.init(baseEmoji: .moon, skinTones: nil) + } else if rawValue == "🌕" { + self.init(baseEmoji: .fullMoon, skinTones: nil) + } else if rawValue == "🌖" { + self.init(baseEmoji: .waningGibbousMoon, skinTones: nil) + } else if rawValue == "🌗" { + self.init(baseEmoji: .lastQuarterMoon, skinTones: nil) + } else if rawValue == "🌘" { + self.init(baseEmoji: .waningCrescentMoon, skinTones: nil) + } else if rawValue == "🌙" { + self.init(baseEmoji: .crescentMoon, skinTones: nil) + } else if rawValue == "🌚" { + self.init(baseEmoji: .newMoonWithFace, skinTones: nil) + } else if rawValue == "🌛" { + self.init(baseEmoji: .firstQuarterMoonWithFace, skinTones: nil) + } else if rawValue == "🌜" { + self.init(baseEmoji: .lastQuarterMoonWithFace, skinTones: nil) + } else if rawValue == "🌡ī¸" { + self.init(baseEmoji: .thermometer, skinTones: nil) + } else if rawValue == "☀ī¸" { + self.init(baseEmoji: .sunny, skinTones: nil) + } else if rawValue == "🌝" { + self.init(baseEmoji: .fullMoonWithFace, skinTones: nil) + } else if rawValue == "🌞" { + self.init(baseEmoji: .sunWithFace, skinTones: nil) + } else if rawValue == "đŸĒ" { + self.init(baseEmoji: .ringedPlanet, skinTones: nil) + } else if rawValue == "⭐" { + self.init(baseEmoji: .star, skinTones: nil) + } else if rawValue == "🌟" { + self.init(baseEmoji: .star2, skinTones: nil) + } else if rawValue == "🌠" { + self.init(baseEmoji: .stars, skinTones: nil) + } else if rawValue == "🌌" { + self.init(baseEmoji: .milkyWay, skinTones: nil) + } else if rawValue == "☁ī¸" { + self.init(baseEmoji: .cloud, skinTones: nil) + } else if rawValue == "⛅" { + self.init(baseEmoji: .partlySunny, skinTones: nil) + } else if rawValue == "⛈ī¸" { + self.init(baseEmoji: .thunderCloudAndRain, skinTones: nil) + } else if rawValue == "🌤ī¸" { + self.init(baseEmoji: .mostlySunny, skinTones: nil) + } else if rawValue == "đŸŒĨī¸" { + self.init(baseEmoji: .barelySunny, skinTones: nil) + } else if rawValue == "đŸŒĻī¸" { + self.init(baseEmoji: .partlySunnyRain, skinTones: nil) + } else if rawValue == "🌧ī¸" { + self.init(baseEmoji: .rainCloud, skinTones: nil) + } else if rawValue == "🌨ī¸" { + self.init(baseEmoji: .snowCloud, skinTones: nil) + } else if rawValue == "🌩ī¸" { + self.init(baseEmoji: .lightning, skinTones: nil) + } else if rawValue == "đŸŒĒī¸" { + self.init(baseEmoji: .tornado, skinTones: nil) + } else if rawValue == "đŸŒĢī¸" { + self.init(baseEmoji: .fog, skinTones: nil) + } else if rawValue == "đŸŒŦī¸" { + self.init(baseEmoji: .windBlowingFace, skinTones: nil) + } else if rawValue == "🌀" { + self.init(baseEmoji: .cyclone, skinTones: nil) + } else if rawValue == "🌈" { + self.init(baseEmoji: .rainbow, skinTones: nil) + } else if rawValue == "🌂" { + self.init(baseEmoji: .closedUmbrella, skinTones: nil) + } else if rawValue == "☂ī¸" { + self.init(baseEmoji: .umbrella, skinTones: nil) + } else if rawValue == "☔" { + self.init(baseEmoji: .umbrellaWithRainDrops, skinTones: nil) + } else if rawValue == "⛱ī¸" { + self.init(baseEmoji: .umbrellaOnGround, skinTones: nil) + } else if rawValue == "⚡" { + self.init(baseEmoji: .zap, skinTones: nil) + } else if rawValue == "❄ī¸" { + self.init(baseEmoji: .snowflake, skinTones: nil) + } else if rawValue == "☃ī¸" { + self.init(baseEmoji: .snowman, skinTones: nil) + } else if rawValue == "⛄" { + self.init(baseEmoji: .snowmanWithoutSnow, skinTones: nil) + } else if rawValue == "☄ī¸" { + self.init(baseEmoji: .comet, skinTones: nil) + } else if rawValue == "đŸ”Ĩ" { + self.init(baseEmoji: .fire, skinTones: nil) + } else if rawValue == "💧" { + self.init(baseEmoji: .droplet, skinTones: nil) + } else if rawValue == "🌊" { + self.init(baseEmoji: .ocean, skinTones: nil) + } else if rawValue == "🎃" { + self.init(baseEmoji: .jackOLantern, skinTones: nil) + } else if rawValue == "🎄" { + self.init(baseEmoji: .christmasTree, skinTones: nil) + } else if rawValue == "🎆" { + self.init(baseEmoji: .fireworks, skinTones: nil) + } else if rawValue == "🎇" { + self.init(baseEmoji: .sparkler, skinTones: nil) + } else if rawValue == "🧨" { + self.init(baseEmoji: .firecracker, skinTones: nil) + } else if rawValue == "✨" { + self.init(baseEmoji: .sparkles, skinTones: nil) + } else if rawValue == "🎈" { + self.init(baseEmoji: .balloon, skinTones: nil) + } else if rawValue == "🎉" { + self.init(baseEmoji: .tada, skinTones: nil) + } else if rawValue == "🎊" { + self.init(baseEmoji: .confettiBall, skinTones: nil) + } else if rawValue == "🎋" { + self.init(baseEmoji: .tanabataTree, skinTones: nil) + } else if rawValue == "🎍" { + self.init(baseEmoji: .bamboo, skinTones: nil) + } else if rawValue == "🎎" { + self.init(baseEmoji: .dolls, skinTones: nil) + } else if rawValue == "🎏" { + self.init(baseEmoji: .flags, skinTones: nil) + } else if rawValue == "🎐" { + self.init(baseEmoji: .windChime, skinTones: nil) + } else if rawValue == "🎑" { + self.init(baseEmoji: .riceScene, skinTones: nil) + } else if rawValue == "🧧" { + self.init(baseEmoji: .redEnvelope, skinTones: nil) + } else if rawValue == "🎀" { + self.init(baseEmoji: .ribbon, skinTones: nil) + } else if rawValue == "🎁" { + self.init(baseEmoji: .gift, skinTones: nil) + } else if rawValue == "🎗ī¸" { + self.init(baseEmoji: .reminderRibbon, skinTones: nil) + } else if rawValue == "🎟ī¸" { + self.init(baseEmoji: .admissionTickets, skinTones: nil) + } else if rawValue == "đŸŽĢ" { + self.init(baseEmoji: .ticket, skinTones: nil) + } else if rawValue == "🎖ī¸" { + self.init(baseEmoji: .medal, skinTones: nil) + } else if rawValue == "🏆" { + self.init(baseEmoji: .trophy, skinTones: nil) + } else if rawValue == "🏅" { + self.init(baseEmoji: .sportsMedal, skinTones: nil) + } else if rawValue == "đŸĨ‡" { + self.init(baseEmoji: .firstPlaceMedal, skinTones: nil) + } else if rawValue == "đŸĨˆ" { + self.init(baseEmoji: .secondPlaceMedal, skinTones: nil) + } else if rawValue == "đŸĨ‰" { + self.init(baseEmoji: .thirdPlaceMedal, skinTones: nil) + } else if rawValue == "âšŊ" { + self.init(baseEmoji: .soccer, skinTones: nil) + } else if rawValue == "⚾" { + self.init(baseEmoji: .baseball, skinTones: nil) + } else if rawValue == "đŸĨŽ" { + self.init(baseEmoji: .softball, skinTones: nil) + } else if rawValue == "🏀" { + self.init(baseEmoji: .basketball, skinTones: nil) + } else if rawValue == "🏐" { + self.init(baseEmoji: .volleyball, skinTones: nil) + } else if rawValue == "🏈" { + self.init(baseEmoji: .football, skinTones: nil) + } else if rawValue == "🏉" { + self.init(baseEmoji: .rugbyFootball, skinTones: nil) + } else if rawValue == "🎾" { + self.init(baseEmoji: .tennis, skinTones: nil) + } else if rawValue == "đŸĨ" { + self.init(baseEmoji: .flyingDisc, skinTones: nil) + } else if rawValue == "đŸŽŗ" { + self.init(baseEmoji: .bowling, skinTones: nil) + } else if rawValue == "🏏" { + self.init(baseEmoji: .cricketBatAndBall, skinTones: nil) + } else if rawValue == "🏑" { + self.init(baseEmoji: .fieldHockeyStickAndBall, skinTones: nil) + } else if rawValue == "🏒" { + self.init(baseEmoji: .iceHockeyStickAndPuck, skinTones: nil) + } else if rawValue == "đŸĨ" { + self.init(baseEmoji: .lacrosse, skinTones: nil) + } else if rawValue == "🏓" { + self.init(baseEmoji: .tableTennisPaddleAndBall, skinTones: nil) + } else if rawValue == "🏸" { + self.init(baseEmoji: .badmintonRacquetAndShuttlecock, skinTones: nil) + } else if rawValue == "đŸĨŠ" { + self.init(baseEmoji: .boxingGlove, skinTones: nil) + } else if rawValue == "đŸĨ‹" { + self.init(baseEmoji: .martialArtsUniform, skinTones: nil) + } else if rawValue == "đŸĨ…" { + self.init(baseEmoji: .goalNet, skinTones: nil) + } else if rawValue == "â›ŗ" { + self.init(baseEmoji: .golf, skinTones: nil) + } else if rawValue == "⛸ī¸" { + self.init(baseEmoji: .iceSkate, skinTones: nil) + } else if rawValue == "đŸŽŖ" { + self.init(baseEmoji: .fishingPoleAndFish, skinTones: nil) + } else if rawValue == "đŸ¤ŋ" { + self.init(baseEmoji: .divingMask, skinTones: nil) + } else if rawValue == "đŸŽŊ" { + self.init(baseEmoji: .runningShirtWithSash, skinTones: nil) + } else if rawValue == "đŸŽŋ" { + self.init(baseEmoji: .ski, skinTones: nil) + } else if rawValue == "🛷" { + self.init(baseEmoji: .sled, skinTones: nil) + } else if rawValue == "đŸĨŒ" { + self.init(baseEmoji: .curlingStone, skinTones: nil) + } else if rawValue == "đŸŽ¯" { + self.init(baseEmoji: .dart, skinTones: nil) + } else if rawValue == "đŸĒ€" { + self.init(baseEmoji: .yoYo, skinTones: nil) + } else if rawValue == "đŸĒ" { + self.init(baseEmoji: .kite, skinTones: nil) + } else if rawValue == "🎱" { + self.init(baseEmoji: .eightBall, skinTones: nil) + } else if rawValue == "🔮" { + self.init(baseEmoji: .crystalBall, skinTones: nil) + } else if rawValue == "đŸĒ„" { + self.init(baseEmoji: .magicWand, skinTones: nil) + } else if rawValue == "đŸ§ŋ" { + self.init(baseEmoji: .nazarAmulet, skinTones: nil) + } else if rawValue == "đŸĒŦ" { + self.init(baseEmoji: .hamsa, skinTones: nil) + } else if rawValue == "🎮" { + self.init(baseEmoji: .videoGame, skinTones: nil) + } else if rawValue == "🕹ī¸" { + self.init(baseEmoji: .joystick, skinTones: nil) + } else if rawValue == "🎰" { + self.init(baseEmoji: .slotMachine, skinTones: nil) + } else if rawValue == "🎲" { + self.init(baseEmoji: .gameDie, skinTones: nil) + } else if rawValue == "🧩" { + self.init(baseEmoji: .jigsaw, skinTones: nil) + } else if rawValue == "🧸" { + self.init(baseEmoji: .teddyBear, skinTones: nil) + } else if rawValue == "đŸĒ…" { + self.init(baseEmoji: .pinata, skinTones: nil) + } else if rawValue == "đŸĒŠ" { + self.init(baseEmoji: .mirrorBall, skinTones: nil) + } else if rawValue == "đŸĒ†" { + self.init(baseEmoji: .nestingDolls, skinTones: nil) + } else if rawValue == "♠ī¸" { + self.init(baseEmoji: .spades, skinTones: nil) + } else if rawValue == "â™Ĩī¸" { + self.init(baseEmoji: .hearts, skinTones: nil) + } else if rawValue == "â™Ļī¸" { + self.init(baseEmoji: .diamonds, skinTones: nil) + } else if rawValue == "â™Ŗī¸" { + self.init(baseEmoji: .clubs, skinTones: nil) + } else if rawValue == "♟ī¸" { + self.init(baseEmoji: .chessPawn, skinTones: nil) + } else if rawValue == "🃏" { + self.init(baseEmoji: .blackJoker, skinTones: nil) + } else if rawValue == "🀄" { + self.init(baseEmoji: .mahjong, skinTones: nil) + } else if rawValue == "🎴" { + self.init(baseEmoji: .flowerPlayingCards, skinTones: nil) + } else if rawValue == "🎭" { + self.init(baseEmoji: .performingArts, skinTones: nil) + } else if rawValue == "đŸ–ŧī¸" { + self.init(baseEmoji: .frameWithPicture, skinTones: nil) + } else if rawValue == "🎨" { + self.init(baseEmoji: .art, skinTones: nil) + } else if rawValue == "đŸ§ĩ" { + self.init(baseEmoji: .thread, skinTones: nil) + } else if rawValue == "đŸĒĄ" { + self.init(baseEmoji: .sewingNeedle, skinTones: nil) + } else if rawValue == "đŸ§ļ" { + self.init(baseEmoji: .yarn, skinTones: nil) + } else if rawValue == "đŸĒĸ" { + self.init(baseEmoji: .knot, skinTones: nil) + } else if rawValue == "👓" { + self.init(baseEmoji: .eyeglasses, skinTones: nil) + } else if rawValue == "đŸ•ļī¸" { + self.init(baseEmoji: .darkSunglasses, skinTones: nil) + } else if rawValue == "đŸĨŊ" { + self.init(baseEmoji: .goggles, skinTones: nil) + } else if rawValue == "đŸĨŧ" { + self.init(baseEmoji: .labCoat, skinTones: nil) + } else if rawValue == "đŸĻē" { + self.init(baseEmoji: .safetyVest, skinTones: nil) + } else if rawValue == "👔" { + self.init(baseEmoji: .necktie, skinTones: nil) + } else if rawValue == "👕" { + self.init(baseEmoji: .shirt, skinTones: nil) + } else if rawValue == "👖" { + self.init(baseEmoji: .jeans, skinTones: nil) + } else if rawValue == "đŸ§Ŗ" { + self.init(baseEmoji: .scarf, skinTones: nil) + } else if rawValue == "🧤" { + self.init(baseEmoji: .gloves, skinTones: nil) + } else if rawValue == "đŸ§Ĩ" { + self.init(baseEmoji: .coat, skinTones: nil) + } else if rawValue == "đŸ§Ļ" { + self.init(baseEmoji: .socks, skinTones: nil) + } else if rawValue == "👗" { + self.init(baseEmoji: .dress, skinTones: nil) + } else if rawValue == "👘" { + self.init(baseEmoji: .kimono, skinTones: nil) + } else if rawValue == "đŸĨģ" { + self.init(baseEmoji: .sari, skinTones: nil) + } else if rawValue == "🩱" { + self.init(baseEmoji: .onePieceSwimsuit, skinTones: nil) + } else if rawValue == "🩲" { + self.init(baseEmoji: .briefs, skinTones: nil) + } else if rawValue == "đŸŠŗ" { + self.init(baseEmoji: .shorts, skinTones: nil) + } else if rawValue == "👙" { + self.init(baseEmoji: .bikini, skinTones: nil) + } else if rawValue == "👚" { + self.init(baseEmoji: .womansClothes, skinTones: nil) + } else if rawValue == "👛" { + self.init(baseEmoji: .purse, skinTones: nil) + } else if rawValue == "👜" { + self.init(baseEmoji: .handbag, skinTones: nil) + } else if rawValue == "👝" { + self.init(baseEmoji: .pouch, skinTones: nil) + } else if rawValue == "🛍ī¸" { + self.init(baseEmoji: .shoppingBags, skinTones: nil) + } else if rawValue == "🎒" { + self.init(baseEmoji: .schoolSatchel, skinTones: nil) + } else if rawValue == "🩴" { + self.init(baseEmoji: .thongSandal, skinTones: nil) + } else if rawValue == "👞" { + self.init(baseEmoji: .mansShoe, skinTones: nil) + } else if rawValue == "👟" { + self.init(baseEmoji: .athleticShoe, skinTones: nil) + } else if rawValue == "đŸĨž" { + self.init(baseEmoji: .hikingBoot, skinTones: nil) + } else if rawValue == "đŸĨŋ" { + self.init(baseEmoji: .womansFlatShoe, skinTones: nil) + } else if rawValue == "👠" { + self.init(baseEmoji: .highHeel, skinTones: nil) + } else if rawValue == "👡" { + self.init(baseEmoji: .sandal, skinTones: nil) + } else if rawValue == "🩰" { + self.init(baseEmoji: .balletShoes, skinTones: nil) + } else if rawValue == "đŸ‘ĸ" { + self.init(baseEmoji: .boot, skinTones: nil) + } else if rawValue == "👑" { + self.init(baseEmoji: .crown, skinTones: nil) + } else if rawValue == "👒" { + self.init(baseEmoji: .womansHat, skinTones: nil) + } else if rawValue == "🎩" { + self.init(baseEmoji: .tophat, skinTones: nil) + } else if rawValue == "🎓" { + self.init(baseEmoji: .mortarBoard, skinTones: nil) + } else if rawValue == "đŸ§ĸ" { + self.init(baseEmoji: .billedCap, skinTones: nil) + } else if rawValue == "đŸĒ–" { + self.init(baseEmoji: .militaryHelmet, skinTones: nil) + } else if rawValue == "⛑ī¸" { + self.init(baseEmoji: .helmetWithWhiteCross, skinTones: nil) + } else if rawValue == "đŸ“ŋ" { + self.init(baseEmoji: .prayerBeads, skinTones: nil) + } else if rawValue == "💄" { + self.init(baseEmoji: .lipstick, skinTones: nil) + } else if rawValue == "💍" { + self.init(baseEmoji: .ring, skinTones: nil) + } else if rawValue == "💎" { + self.init(baseEmoji: .gem, skinTones: nil) + } else if rawValue == "🔇" { + self.init(baseEmoji: .mute, skinTones: nil) + } else if rawValue == "🔈" { + self.init(baseEmoji: .speaker, skinTones: nil) + } else if rawValue == "🔉" { + self.init(baseEmoji: .sound, skinTones: nil) + } else if rawValue == "🔊" { + self.init(baseEmoji: .loudSound, skinTones: nil) + } else if rawValue == "đŸ“ĸ" { + self.init(baseEmoji: .loudspeaker, skinTones: nil) + } else if rawValue == "đŸ“Ŗ" { + self.init(baseEmoji: .mega, skinTones: nil) + } else if rawValue == "đŸ“¯" { + self.init(baseEmoji: .postalHorn, skinTones: nil) + } else if rawValue == "🔔" { + self.init(baseEmoji: .bell, skinTones: nil) + } else if rawValue == "🔕" { + self.init(baseEmoji: .noBell, skinTones: nil) + } else if rawValue == "đŸŽŧ" { + self.init(baseEmoji: .musicalScore, skinTones: nil) + } else if rawValue == "đŸŽĩ" { + self.init(baseEmoji: .musicalNote, skinTones: nil) + } else if rawValue == "đŸŽļ" { + self.init(baseEmoji: .notes, skinTones: nil) + } else if rawValue == "🎙ī¸" { + self.init(baseEmoji: .studioMicrophone, skinTones: nil) + } else if rawValue == "🎚ī¸" { + self.init(baseEmoji: .levelSlider, skinTones: nil) + } else if rawValue == "🎛ī¸" { + self.init(baseEmoji: .controlKnobs, skinTones: nil) + } else if rawValue == "🎤" { + self.init(baseEmoji: .microphone, skinTones: nil) + } else if rawValue == "🎧" { + self.init(baseEmoji: .headphones, skinTones: nil) + } else if rawValue == "đŸ“ģ" { + self.init(baseEmoji: .radio, skinTones: nil) + } else if rawValue == "🎷" { + self.init(baseEmoji: .saxophone, skinTones: nil) + } else if rawValue == "đŸĒ—" { + self.init(baseEmoji: .accordion, skinTones: nil) + } else if rawValue == "🎸" { + self.init(baseEmoji: .guitar, skinTones: nil) + } else if rawValue == "🎹" { + self.init(baseEmoji: .musicalKeyboard, skinTones: nil) + } else if rawValue == "đŸŽē" { + self.init(baseEmoji: .trumpet, skinTones: nil) + } else if rawValue == "đŸŽģ" { + self.init(baseEmoji: .violin, skinTones: nil) + } else if rawValue == "đŸĒ•" { + self.init(baseEmoji: .banjo, skinTones: nil) + } else if rawValue == "đŸĨ" { + self.init(baseEmoji: .drumWithDrumsticks, skinTones: nil) + } else if rawValue == "đŸĒ˜" { + self.init(baseEmoji: .longDrum, skinTones: nil) + } else if rawValue == "📱" { + self.init(baseEmoji: .iphone, skinTones: nil) + } else if rawValue == "📲" { + self.init(baseEmoji: .calling, skinTones: nil) + } else if rawValue == "☎ī¸" { + self.init(baseEmoji: .phone, skinTones: nil) + } else if rawValue == "📞" { + self.init(baseEmoji: .telephoneReceiver, skinTones: nil) + } else if rawValue == "📟" { + self.init(baseEmoji: .pager, skinTones: nil) + } else if rawValue == "📠" { + self.init(baseEmoji: .fax, skinTones: nil) + } else if rawValue == "🔋" { + self.init(baseEmoji: .battery, skinTones: nil) + } else if rawValue == "đŸĒĢ" { + self.init(baseEmoji: .lowBattery, skinTones: nil) + } else if rawValue == "🔌" { + self.init(baseEmoji: .electricPlug, skinTones: nil) + } else if rawValue == "đŸ’ģ" { + self.init(baseEmoji: .computer, skinTones: nil) + } else if rawValue == "đŸ–Ĩī¸" { + self.init(baseEmoji: .desktopComputer, skinTones: nil) + } else if rawValue == "🖨ī¸" { + self.init(baseEmoji: .printer, skinTones: nil) + } else if rawValue == "⌨ī¸" { + self.init(baseEmoji: .keyboard, skinTones: nil) + } else if rawValue == "🖱ī¸" { + self.init(baseEmoji: .threeButtonMouse, skinTones: nil) + } else if rawValue == "🖲ī¸" { + self.init(baseEmoji: .trackball, skinTones: nil) + } else if rawValue == "đŸ’Ŋ" { + self.init(baseEmoji: .minidisc, skinTones: nil) + } else if rawValue == "💾" { + self.init(baseEmoji: .floppyDisk, skinTones: nil) + } else if rawValue == "đŸ’ŋ" { + self.init(baseEmoji: .cd, skinTones: nil) + } else if rawValue == "📀" { + self.init(baseEmoji: .dvd, skinTones: nil) + } else if rawValue == "🧮" { + self.init(baseEmoji: .abacus, skinTones: nil) + } else if rawValue == "đŸŽĨ" { + self.init(baseEmoji: .movieCamera, skinTones: nil) + } else if rawValue == "🎞ī¸" { + self.init(baseEmoji: .filmFrames, skinTones: nil) + } else if rawValue == "đŸ“Ŋī¸" { + self.init(baseEmoji: .filmProjector, skinTones: nil) + } else if rawValue == "đŸŽŦ" { + self.init(baseEmoji: .clapper, skinTones: nil) + } else if rawValue == "đŸ“ē" { + self.init(baseEmoji: .tv, skinTones: nil) + } else if rawValue == "📷" { + self.init(baseEmoji: .camera, skinTones: nil) + } else if rawValue == "📸" { + self.init(baseEmoji: .cameraWithFlash, skinTones: nil) + } else if rawValue == "📹" { + self.init(baseEmoji: .videoCamera, skinTones: nil) + } else if rawValue == "đŸ“ŧ" { + self.init(baseEmoji: .vhs, skinTones: nil) + } else if rawValue == "🔍" { + self.init(baseEmoji: .mag, skinTones: nil) + } else if rawValue == "🔎" { + self.init(baseEmoji: .magRight, skinTones: nil) + } else if rawValue == "đŸ•¯ī¸" { + self.init(baseEmoji: .candle, skinTones: nil) + } else if rawValue == "💡" { + self.init(baseEmoji: .bulb, skinTones: nil) + } else if rawValue == "đŸ”Ļ" { + self.init(baseEmoji: .flashlight, skinTones: nil) + } else if rawValue == "🏮" { + self.init(baseEmoji: .izakayaLantern, skinTones: nil) + } else if rawValue == "đŸĒ”" { + self.init(baseEmoji: .diyaLamp, skinTones: nil) + } else if rawValue == "📔" { + self.init(baseEmoji: .notebookWithDecorativeCover, skinTones: nil) + } else if rawValue == "📕" { + self.init(baseEmoji: .closedBook, skinTones: nil) + } else if rawValue == "📖" { + self.init(baseEmoji: .book, skinTones: nil) + } else if rawValue == "📗" { + self.init(baseEmoji: .greenBook, skinTones: nil) + } else if rawValue == "📘" { + self.init(baseEmoji: .blueBook, skinTones: nil) + } else if rawValue == "📙" { + self.init(baseEmoji: .orangeBook, skinTones: nil) + } else if rawValue == "📚" { + self.init(baseEmoji: .books, skinTones: nil) + } else if rawValue == "📓" { + self.init(baseEmoji: .notebook, skinTones: nil) + } else if rawValue == "📒" { + self.init(baseEmoji: .ledger, skinTones: nil) + } else if rawValue == "📃" { + self.init(baseEmoji: .pageWithCurl, skinTones: nil) + } else if rawValue == "📜" { + self.init(baseEmoji: .scroll, skinTones: nil) + } else if rawValue == "📄" { + self.init(baseEmoji: .pageFacingUp, skinTones: nil) + } else if rawValue == "📰" { + self.init(baseEmoji: .newspaper, skinTones: nil) + } else if rawValue == "🗞ī¸" { + self.init(baseEmoji: .rolledUpNewspaper, skinTones: nil) + } else if rawValue == "📑" { + self.init(baseEmoji: .bookmarkTabs, skinTones: nil) + } else if rawValue == "🔖" { + self.init(baseEmoji: .bookmark, skinTones: nil) + } else if rawValue == "🏷ī¸" { + self.init(baseEmoji: .label, skinTones: nil) + } else if rawValue == "💰" { + self.init(baseEmoji: .moneybag, skinTones: nil) + } else if rawValue == "đŸĒ™" { + self.init(baseEmoji: .coin, skinTones: nil) + } else if rawValue == "💴" { + self.init(baseEmoji: .yen, skinTones: nil) + } else if rawValue == "đŸ’ĩ" { + self.init(baseEmoji: .dollar, skinTones: nil) + } else if rawValue == "đŸ’ļ" { + self.init(baseEmoji: .euro, skinTones: nil) + } else if rawValue == "💷" { + self.init(baseEmoji: .pound, skinTones: nil) + } else if rawValue == "💸" { + self.init(baseEmoji: .moneyWithWings, skinTones: nil) + } else if rawValue == "đŸ’ŗ" { + self.init(baseEmoji: .creditCard, skinTones: nil) + } else if rawValue == "🧾" { + self.init(baseEmoji: .receipt, skinTones: nil) + } else if rawValue == "💹" { + self.init(baseEmoji: .chart, skinTones: nil) + } else if rawValue == "✉ī¸" { + self.init(baseEmoji: .email, skinTones: nil) + } else if rawValue == "📧" { + self.init(baseEmoji: .eMail, skinTones: nil) + } else if rawValue == "📨" { + self.init(baseEmoji: .incomingEnvelope, skinTones: nil) + } else if rawValue == "📩" { + self.init(baseEmoji: .envelopeWithArrow, skinTones: nil) + } else if rawValue == "📤" { + self.init(baseEmoji: .outboxTray, skinTones: nil) + } else if rawValue == "đŸ“Ĩ" { + self.init(baseEmoji: .inboxTray, skinTones: nil) + } else if rawValue == "đŸ“Ļ" { + self.init(baseEmoji: .package, skinTones: nil) + } else if rawValue == "đŸ“Ģ" { + self.init(baseEmoji: .mailbox, skinTones: nil) + } else if rawValue == "đŸ“Ē" { + self.init(baseEmoji: .mailboxClosed, skinTones: nil) + } else if rawValue == "đŸ“Ŧ" { + self.init(baseEmoji: .mailboxWithMail, skinTones: nil) + } else if rawValue == "📭" { + self.init(baseEmoji: .mailboxWithNoMail, skinTones: nil) + } else if rawValue == "📮" { + self.init(baseEmoji: .postbox, skinTones: nil) + } else if rawValue == "đŸ—ŗī¸" { + self.init(baseEmoji: .ballotBoxWithBallot, skinTones: nil) + } else if rawValue == "✏ī¸" { + self.init(baseEmoji: .pencil2, skinTones: nil) + } else if rawValue == "✒ī¸" { + self.init(baseEmoji: .blackNib, skinTones: nil) + } else if rawValue == "🖋ī¸" { + self.init(baseEmoji: .lowerLeftFountainPen, skinTones: nil) + } else if rawValue == "🖊ī¸" { + self.init(baseEmoji: .lowerLeftBallpointPen, skinTones: nil) + } else if rawValue == "🖌ī¸" { + self.init(baseEmoji: .lowerLeftPaintbrush, skinTones: nil) + } else if rawValue == "🖍ī¸" { + self.init(baseEmoji: .lowerLeftCrayon, skinTones: nil) + } else if rawValue == "📝" { + self.init(baseEmoji: .memo, skinTones: nil) + } else if rawValue == "đŸ’ŧ" { + self.init(baseEmoji: .briefcase, skinTones: nil) + } else if rawValue == "📁" { + self.init(baseEmoji: .fileFolder, skinTones: nil) + } else if rawValue == "📂" { + self.init(baseEmoji: .openFileFolder, skinTones: nil) + } else if rawValue == "🗂ī¸" { + self.init(baseEmoji: .cardIndexDividers, skinTones: nil) + } else if rawValue == "📅" { + self.init(baseEmoji: .date, skinTones: nil) + } else if rawValue == "📆" { + self.init(baseEmoji: .calendar, skinTones: nil) + } else if rawValue == "🗒ī¸" { + self.init(baseEmoji: .spiralNotePad, skinTones: nil) + } else if rawValue == "🗓ī¸" { + self.init(baseEmoji: .spiralCalendarPad, skinTones: nil) + } else if rawValue == "📇" { + self.init(baseEmoji: .cardIndex, skinTones: nil) + } else if rawValue == "📈" { + self.init(baseEmoji: .chartWithUpwardsTrend, skinTones: nil) + } else if rawValue == "📉" { + self.init(baseEmoji: .chartWithDownwardsTrend, skinTones: nil) + } else if rawValue == "📊" { + self.init(baseEmoji: .barChart, skinTones: nil) + } else if rawValue == "📋" { + self.init(baseEmoji: .clipboard, skinTones: nil) + } else if rawValue == "📌" { + self.init(baseEmoji: .pushpin, skinTones: nil) + } else if rawValue == "📍" { + self.init(baseEmoji: .roundPushpin, skinTones: nil) + } else if rawValue == "📎" { + self.init(baseEmoji: .paperclip, skinTones: nil) + } else if rawValue == "🖇ī¸" { + self.init(baseEmoji: .linkedPaperclips, skinTones: nil) + } else if rawValue == "📏" { + self.init(baseEmoji: .straightRuler, skinTones: nil) + } else if rawValue == "📐" { + self.init(baseEmoji: .triangularRuler, skinTones: nil) + } else if rawValue == "✂ī¸" { + self.init(baseEmoji: .scissors, skinTones: nil) + } else if rawValue == "🗃ī¸" { + self.init(baseEmoji: .cardFileBox, skinTones: nil) + } else if rawValue == "🗄ī¸" { + self.init(baseEmoji: .fileCabinet, skinTones: nil) + } else if rawValue == "🗑ī¸" { + self.init(baseEmoji: .wastebasket, skinTones: nil) + } else if rawValue == "🔒" { + self.init(baseEmoji: .lock, skinTones: nil) + } else if rawValue == "🔓" { + self.init(baseEmoji: .unlock, skinTones: nil) + } else if rawValue == "🔏" { + self.init(baseEmoji: .lockWithInkPen, skinTones: nil) + } else if rawValue == "🔐" { + self.init(baseEmoji: .closedLockWithKey, skinTones: nil) + } else if rawValue == "🔑" { + self.init(baseEmoji: .key, skinTones: nil) + } else if rawValue == "🗝ī¸" { + self.init(baseEmoji: .oldKey, skinTones: nil) + } else if rawValue == "🔨" { + self.init(baseEmoji: .hammer, skinTones: nil) + } else if rawValue == "đŸĒ“" { + self.init(baseEmoji: .axe, skinTones: nil) + } else if rawValue == "⛏ī¸" { + self.init(baseEmoji: .pick, skinTones: nil) + } else if rawValue == "⚒ī¸" { + self.init(baseEmoji: .hammerAndPick, skinTones: nil) + } else if rawValue == "🛠ī¸" { + self.init(baseEmoji: .hammerAndWrench, skinTones: nil) + } else if rawValue == "🗡ī¸" { + self.init(baseEmoji: .daggerKnife, skinTones: nil) + } else if rawValue == "⚔ī¸" { + self.init(baseEmoji: .crossedSwords, skinTones: nil) + } else if rawValue == "đŸ”Ģ" { + self.init(baseEmoji: .gun, skinTones: nil) + } else if rawValue == "đŸĒƒ" { + self.init(baseEmoji: .boomerang, skinTones: nil) + } else if rawValue == "🏹" { + self.init(baseEmoji: .bowAndArrow, skinTones: nil) + } else if rawValue == "🛡ī¸" { + self.init(baseEmoji: .shield, skinTones: nil) + } else if rawValue == "đŸĒš" { + self.init(baseEmoji: .carpentrySaw, skinTones: nil) + } else if rawValue == "🔧" { + self.init(baseEmoji: .wrench, skinTones: nil) + } else if rawValue == "đŸĒ›" { + self.init(baseEmoji: .screwdriver, skinTones: nil) + } else if rawValue == "🔩" { + self.init(baseEmoji: .nutAndBolt, skinTones: nil) + } else if rawValue == "⚙ī¸" { + self.init(baseEmoji: .gear, skinTones: nil) + } else if rawValue == "🗜ī¸" { + self.init(baseEmoji: .compression, skinTones: nil) + } else if rawValue == "⚖ī¸" { + self.init(baseEmoji: .scales, skinTones: nil) + } else if rawValue == "đŸĻ¯" { + self.init(baseEmoji: .probingCane, skinTones: nil) + } else if rawValue == "🔗" { + self.init(baseEmoji: .link, skinTones: nil) + } else if rawValue == "⛓ī¸" { + self.init(baseEmoji: .chains, skinTones: nil) + } else if rawValue == "đŸĒ" { + self.init(baseEmoji: .hook, skinTones: nil) + } else if rawValue == "🧰" { + self.init(baseEmoji: .toolbox, skinTones: nil) + } else if rawValue == "🧲" { + self.init(baseEmoji: .magnet, skinTones: nil) + } else if rawValue == "đŸĒœ" { + self.init(baseEmoji: .ladder, skinTones: nil) + } else if rawValue == "⚗ī¸" { + self.init(baseEmoji: .alembic, skinTones: nil) + } else if rawValue == "đŸ§Ē" { + self.init(baseEmoji: .testTube, skinTones: nil) + } else if rawValue == "đŸ§Ģ" { + self.init(baseEmoji: .petriDish, skinTones: nil) + } else if rawValue == "đŸ§Ŧ" { + self.init(baseEmoji: .dna, skinTones: nil) + } else if rawValue == "đŸ”Ŧ" { + self.init(baseEmoji: .microscope, skinTones: nil) + } else if rawValue == "🔭" { + self.init(baseEmoji: .telescope, skinTones: nil) + } else if rawValue == "📡" { + self.init(baseEmoji: .satelliteAntenna, skinTones: nil) + } else if rawValue == "💉" { + self.init(baseEmoji: .syringe, skinTones: nil) + } else if rawValue == "🩸" { + self.init(baseEmoji: .dropOfBlood, skinTones: nil) + } else if rawValue == "💊" { + self.init(baseEmoji: .pill, skinTones: nil) + } else if rawValue == "🩹" { + self.init(baseEmoji: .adhesiveBandage, skinTones: nil) + } else if rawValue == "đŸŠŧ" { + self.init(baseEmoji: .crutch, skinTones: nil) + } else if rawValue == "đŸŠē" { + self.init(baseEmoji: .stethoscope, skinTones: nil) + } else if rawValue == "đŸŠģ" { + self.init(baseEmoji: .xRay, skinTones: nil) + } else if rawValue == "đŸšĒ" { + self.init(baseEmoji: .door, skinTones: nil) + } else if rawValue == "🛗" { + self.init(baseEmoji: .elevator, skinTones: nil) + } else if rawValue == "đŸĒž" { + self.init(baseEmoji: .mirror, skinTones: nil) + } else if rawValue == "đŸĒŸ" { + self.init(baseEmoji: .window, skinTones: nil) + } else if rawValue == "🛏ī¸" { + self.init(baseEmoji: .bed, skinTones: nil) + } else if rawValue == "🛋ī¸" { + self.init(baseEmoji: .couchAndLamp, skinTones: nil) + } else if rawValue == "đŸĒ‘" { + self.init(baseEmoji: .chair, skinTones: nil) + } else if rawValue == "đŸšŊ" { + self.init(baseEmoji: .toilet, skinTones: nil) + } else if rawValue == "đŸĒ " { + self.init(baseEmoji: .plunger, skinTones: nil) + } else if rawValue == "đŸšŋ" { + self.init(baseEmoji: .shower, skinTones: nil) + } else if rawValue == "🛁" { + self.init(baseEmoji: .bathtub, skinTones: nil) + } else if rawValue == "đŸĒ¤" { + self.init(baseEmoji: .mouseTrap, skinTones: nil) + } else if rawValue == "đŸĒ’" { + self.init(baseEmoji: .razor, skinTones: nil) + } else if rawValue == "🧴" { + self.init(baseEmoji: .lotionBottle, skinTones: nil) + } else if rawValue == "🧷" { + self.init(baseEmoji: .safetyPin, skinTones: nil) + } else if rawValue == "🧹" { + self.init(baseEmoji: .broom, skinTones: nil) + } else if rawValue == "đŸ§ē" { + self.init(baseEmoji: .basket, skinTones: nil) + } else if rawValue == "đŸ§ģ" { + self.init(baseEmoji: .rollOfPaper, skinTones: nil) + } else if rawValue == "đŸĒŖ" { + self.init(baseEmoji: .bucket, skinTones: nil) + } else if rawValue == "đŸ§ŧ" { + self.init(baseEmoji: .soap, skinTones: nil) + } else if rawValue == "đŸĢ§" { + self.init(baseEmoji: .bubbles, skinTones: nil) + } else if rawValue == "đŸĒĨ" { + self.init(baseEmoji: .toothbrush, skinTones: nil) + } else if rawValue == "đŸ§Ŋ" { + self.init(baseEmoji: .sponge, skinTones: nil) + } else if rawValue == "đŸ§¯" { + self.init(baseEmoji: .fireExtinguisher, skinTones: nil) + } else if rawValue == "🛒" { + self.init(baseEmoji: .shoppingTrolley, skinTones: nil) + } else if rawValue == "đŸšŦ" { + self.init(baseEmoji: .smoking, skinTones: nil) + } else if rawValue == "⚰ī¸" { + self.init(baseEmoji: .coffin, skinTones: nil) + } else if rawValue == "đŸĒĻ" { + self.init(baseEmoji: .headstone, skinTones: nil) + } else if rawValue == "⚱ī¸" { + self.init(baseEmoji: .funeralUrn, skinTones: nil) + } else if rawValue == "đŸ—ŋ" { + self.init(baseEmoji: .moyai, skinTones: nil) + } else if rawValue == "đŸĒ§" { + self.init(baseEmoji: .placard, skinTones: nil) + } else if rawValue == "đŸĒĒ" { + self.init(baseEmoji: .identificationCard, skinTones: nil) + } else if rawValue == "🏧" { + self.init(baseEmoji: .atm, skinTones: nil) + } else if rawValue == "🚮" { + self.init(baseEmoji: .putLitterInItsPlace, skinTones: nil) + } else if rawValue == "🚰" { + self.init(baseEmoji: .potableWater, skinTones: nil) + } else if rawValue == "â™ŋ" { + self.init(baseEmoji: .wheelchair, skinTones: nil) + } else if rawValue == "🚹" { + self.init(baseEmoji: .mens, skinTones: nil) + } else if rawValue == "đŸšē" { + self.init(baseEmoji: .womens, skinTones: nil) + } else if rawValue == "đŸšģ" { + self.init(baseEmoji: .restroom, skinTones: nil) + } else if rawValue == "đŸšŧ" { + self.init(baseEmoji: .babySymbol, skinTones: nil) + } else if rawValue == "🚾" { + self.init(baseEmoji: .wc, skinTones: nil) + } else if rawValue == "🛂" { + self.init(baseEmoji: .passportControl, skinTones: nil) + } else if rawValue == "🛃" { + self.init(baseEmoji: .customs, skinTones: nil) + } else if rawValue == "🛄" { + self.init(baseEmoji: .baggageClaim, skinTones: nil) + } else if rawValue == "🛅" { + self.init(baseEmoji: .leftLuggage, skinTones: nil) + } else if rawValue == "⚠ī¸" { + self.init(baseEmoji: .warning, skinTones: nil) + } else if rawValue == "🚸" { + self.init(baseEmoji: .childrenCrossing, skinTones: nil) + } else if rawValue == "⛔" { + self.init(baseEmoji: .noEntry, skinTones: nil) + } else if rawValue == "đŸšĢ" { + self.init(baseEmoji: .noEntrySign, skinTones: nil) + } else if rawValue == "đŸšŗ" { + self.init(baseEmoji: .noBicycles, skinTones: nil) + } else if rawValue == "🚭" { + self.init(baseEmoji: .noSmoking, skinTones: nil) + } else if rawValue == "đŸš¯" { + self.init(baseEmoji: .doNotLitter, skinTones: nil) + } else if rawValue == "🚱" { + self.init(baseEmoji: .nonPotableWater, skinTones: nil) + } else if rawValue == "🚷" { + self.init(baseEmoji: .noPedestrians, skinTones: nil) + } else if rawValue == "đŸ“ĩ" { + self.init(baseEmoji: .noMobilePhones, skinTones: nil) + } else if rawValue == "🔞" { + self.init(baseEmoji: .underage, skinTones: nil) + } else if rawValue == "â˜ĸī¸" { + self.init(baseEmoji: .radioactiveSign, skinTones: nil) + } else if rawValue == "â˜Ŗī¸" { + self.init(baseEmoji: .biohazardSign, skinTones: nil) + } else if rawValue == "âŦ†ī¸" { + self.init(baseEmoji: .arrowUp, skinTones: nil) + } else if rawValue == "↗ī¸" { + self.init(baseEmoji: .arrowUpperRight, skinTones: nil) + } else if rawValue == "➡ī¸" { + self.init(baseEmoji: .arrowRight, skinTones: nil) + } else if rawValue == "↘ī¸" { + self.init(baseEmoji: .arrowLowerRight, skinTones: nil) + } else if rawValue == "âŦ‡ī¸" { + self.init(baseEmoji: .arrowDown, skinTones: nil) + } else if rawValue == "↙ī¸" { + self.init(baseEmoji: .arrowLowerLeft, skinTones: nil) + } else if rawValue == "âŦ…ī¸" { + self.init(baseEmoji: .arrowLeft, skinTones: nil) + } else if rawValue == "↖ī¸" { + self.init(baseEmoji: .arrowUpperLeft, skinTones: nil) + } else if rawValue == "↕ī¸" { + self.init(baseEmoji: .arrowUpDown, skinTones: nil) + } else if rawValue == "↔ī¸" { + self.init(baseEmoji: .leftRightArrow, skinTones: nil) + } else if rawValue == "↩ī¸" { + self.init(baseEmoji: .leftwardsArrowWithHook, skinTones: nil) + } else if rawValue == "â†Ēī¸" { + self.init(baseEmoji: .arrowRightHook, skinTones: nil) + } else if rawValue == "⤴ī¸" { + self.init(baseEmoji: .arrowHeadingUp, skinTones: nil) + } else if rawValue == "â¤ĩī¸" { + self.init(baseEmoji: .arrowHeadingDown, skinTones: nil) + } else if rawValue == "🔃" { + self.init(baseEmoji: .arrowsClockwise, skinTones: nil) + } else if rawValue == "🔄" { + self.init(baseEmoji: .arrowsCounterclockwise, skinTones: nil) + } else if rawValue == "🔙" { + self.init(baseEmoji: .back, skinTones: nil) + } else if rawValue == "🔚" { + self.init(baseEmoji: .end, skinTones: nil) + } else if rawValue == "🔛" { + self.init(baseEmoji: .on, skinTones: nil) + } else if rawValue == "🔜" { + self.init(baseEmoji: .soon, skinTones: nil) + } else if rawValue == "🔝" { + self.init(baseEmoji: .top, skinTones: nil) + } else if rawValue == "🛐" { + self.init(baseEmoji: .placeOfWorship, skinTones: nil) + } else if rawValue == "⚛ī¸" { + self.init(baseEmoji: .atomSymbol, skinTones: nil) + } else if rawValue == "🕉ī¸" { + self.init(baseEmoji: .omSymbol, skinTones: nil) + } else if rawValue == "✡ī¸" { + self.init(baseEmoji: .starOfDavid, skinTones: nil) + } else if rawValue == "☸ī¸" { + self.init(baseEmoji: .wheelOfDharma, skinTones: nil) + } else if rawValue == "☯ī¸" { + self.init(baseEmoji: .yinYang, skinTones: nil) + } else if rawValue == "✝ī¸" { + self.init(baseEmoji: .latinCross, skinTones: nil) + } else if rawValue == "â˜Ļī¸" { + self.init(baseEmoji: .orthodoxCross, skinTones: nil) + } else if rawValue == "â˜Ēī¸" { + self.init(baseEmoji: .starAndCrescent, skinTones: nil) + } else if rawValue == "☎ī¸" { + self.init(baseEmoji: .peaceSymbol, skinTones: nil) + } else if rawValue == "🕎" { + self.init(baseEmoji: .menorahWithNineBranches, skinTones: nil) + } else if rawValue == "đŸ”¯" { + self.init(baseEmoji: .sixPointedStar, skinTones: nil) + } else if rawValue == "♈" { + self.init(baseEmoji: .aries, skinTones: nil) + } else if rawValue == "♉" { + self.init(baseEmoji: .taurus, skinTones: nil) + } else if rawValue == "♊" { + self.init(baseEmoji: .gemini, skinTones: nil) + } else if rawValue == "♋" { + self.init(baseEmoji: .cancer, skinTones: nil) + } else if rawValue == "♌" { + self.init(baseEmoji: .leo, skinTones: nil) + } else if rawValue == "♍" { + self.init(baseEmoji: .virgo, skinTones: nil) + } else if rawValue == "♎" { + self.init(baseEmoji: .libra, skinTones: nil) + } else if rawValue == "♏" { + self.init(baseEmoji: .scorpius, skinTones: nil) + } else if rawValue == "♐" { + self.init(baseEmoji: .sagittarius, skinTones: nil) + } else if rawValue == "♑" { + self.init(baseEmoji: .capricorn, skinTones: nil) + } else if rawValue == "♒" { + self.init(baseEmoji: .aquarius, skinTones: nil) + } else if rawValue == "♓" { + self.init(baseEmoji: .pisces, skinTones: nil) + } else if rawValue == "⛎" { + self.init(baseEmoji: .ophiuchus, skinTones: nil) + } else if rawValue == "🔀" { + self.init(baseEmoji: .twistedRightwardsArrows, skinTones: nil) + } else if rawValue == "🔁" { + self.init(baseEmoji: .`repeat`, skinTones: nil) + } else if rawValue == "🔂" { + self.init(baseEmoji: .repeatOne, skinTones: nil) + } else if rawValue == "â–ļī¸" { + self.init(baseEmoji: .arrowForward, skinTones: nil) + } else if rawValue == "⏊" { + self.init(baseEmoji: .fastForward, skinTones: nil) + } else if rawValue == "⏭ī¸" { + self.init(baseEmoji: .blackRightPointingDoubleTriangleWithVerticalBar, skinTones: nil) + } else if rawValue == "⏯ī¸" { + self.init(baseEmoji: .blackRightPointingTriangleWithDoubleVerticalBar, skinTones: nil) + } else if rawValue == "◀ī¸" { + self.init(baseEmoji: .arrowBackward, skinTones: nil) + } else if rawValue == "âĒ" { + self.init(baseEmoji: .rewind, skinTones: nil) + } else if rawValue == "⏎ī¸" { + self.init(baseEmoji: .blackLeftPointingDoubleTriangleWithVerticalBar, skinTones: nil) + } else if rawValue == "đŸ”ŧ" { + self.init(baseEmoji: .arrowUpSmall, skinTones: nil) + } else if rawValue == "âĢ" { + self.init(baseEmoji: .arrowDoubleUp, skinTones: nil) + } else if rawValue == "đŸ”Ŋ" { + self.init(baseEmoji: .arrowDownSmall, skinTones: nil) + } else if rawValue == "âŦ" { + self.init(baseEmoji: .arrowDoubleDown, skinTones: nil) + } else if rawValue == "⏸ī¸" { + self.init(baseEmoji: .doubleVerticalBar, skinTones: nil) + } else if rawValue == "⏚ī¸" { + self.init(baseEmoji: .blackSquareForStop, skinTones: nil) + } else if rawValue == "âēī¸" { + self.init(baseEmoji: .blackCircleForRecord, skinTones: nil) + } else if rawValue == "⏏ī¸" { + self.init(baseEmoji: .eject, skinTones: nil) + } else if rawValue == "đŸŽĻ" { + self.init(baseEmoji: .cinema, skinTones: nil) + } else if rawValue == "🔅" { + self.init(baseEmoji: .lowBrightness, skinTones: nil) + } else if rawValue == "🔆" { + self.init(baseEmoji: .highBrightness, skinTones: nil) + } else if rawValue == "đŸ“ļ" { + self.init(baseEmoji: .signalStrength, skinTones: nil) + } else if rawValue == "đŸ“ŗ" { + self.init(baseEmoji: .vibrationMode, skinTones: nil) + } else if rawValue == "📴" { + self.init(baseEmoji: .mobilePhoneOff, skinTones: nil) + } else if rawValue == "♀ī¸" { + self.init(baseEmoji: .femaleSign, skinTones: nil) + } else if rawValue == "♂ī¸" { + self.init(baseEmoji: .maleSign, skinTones: nil) + } else if rawValue == "⚧ī¸" { + self.init(baseEmoji: .transgenderSymbol, skinTones: nil) + } else if rawValue == "✖ī¸" { + self.init(baseEmoji: .heavyMultiplicationX, skinTones: nil) + } else if rawValue == "➕" { + self.init(baseEmoji: .heavyPlusSign, skinTones: nil) + } else if rawValue == "➖" { + self.init(baseEmoji: .heavyMinusSign, skinTones: nil) + } else if rawValue == "➗" { + self.init(baseEmoji: .heavyDivisionSign, skinTones: nil) + } else if rawValue == "🟰" { + self.init(baseEmoji: .heavyEqualsSign, skinTones: nil) + } else if rawValue == "♾ī¸" { + self.init(baseEmoji: .infinity, skinTones: nil) + } else if rawValue == "â€ŧī¸" { + self.init(baseEmoji: .bangbang, skinTones: nil) + } else if rawValue == "⁉ī¸" { + self.init(baseEmoji: .interrobang, skinTones: nil) + } else if rawValue == "❓" { + self.init(baseEmoji: .question, skinTones: nil) + } else if rawValue == "❔" { + self.init(baseEmoji: .greyQuestion, skinTones: nil) + } else if rawValue == "❕" { + self.init(baseEmoji: .greyExclamation, skinTones: nil) + } else if rawValue == "❗" { + self.init(baseEmoji: .exclamation, skinTones: nil) + } else if rawValue == "〰ī¸" { + self.init(baseEmoji: .wavyDash, skinTones: nil) + } else if rawValue == "💱" { + self.init(baseEmoji: .currencyExchange, skinTones: nil) + } else if rawValue == "💲" { + self.init(baseEmoji: .heavyDollarSign, skinTones: nil) + } else if rawValue == "⚕ī¸" { + self.init(baseEmoji: .medicalSymbol, skinTones: nil) + } else if rawValue == "â™ģī¸" { + self.init(baseEmoji: .recycle, skinTones: nil) + } else if rawValue == "⚜ī¸" { + self.init(baseEmoji: .fleurDeLis, skinTones: nil) + } else if rawValue == "🔱" { + self.init(baseEmoji: .trident, skinTones: nil) + } else if rawValue == "📛" { + self.init(baseEmoji: .nameBadge, skinTones: nil) + } else if rawValue == "🔰" { + self.init(baseEmoji: .beginner, skinTones: nil) + } else if rawValue == "⭕" { + self.init(baseEmoji: .o, skinTones: nil) + } else if rawValue == "✅" { + self.init(baseEmoji: .whiteCheckMark, skinTones: nil) + } else if rawValue == "☑ī¸" { + self.init(baseEmoji: .ballotBoxWithCheck, skinTones: nil) + } else if rawValue == "✔ī¸" { + self.init(baseEmoji: .heavyCheckMark, skinTones: nil) + } else if rawValue == "❌" { + self.init(baseEmoji: .x, skinTones: nil) + } else if rawValue == "❎" { + self.init(baseEmoji: .negativeSquaredCrossMark, skinTones: nil) + } else if rawValue == "➰" { + self.init(baseEmoji: .curlyLoop, skinTones: nil) + } else if rawValue == "âžŋ" { + self.init(baseEmoji: .loop, skinTones: nil) + } else if rawValue == "ã€Ŋī¸" { + self.init(baseEmoji: .partAlternationMark, skinTones: nil) + } else if rawValue == "âœŗī¸" { + self.init(baseEmoji: .eightSpokedAsterisk, skinTones: nil) + } else if rawValue == "✴ī¸" { + self.init(baseEmoji: .eightPointedBlackStar, skinTones: nil) + } else if rawValue == "❇ī¸" { + self.init(baseEmoji: .sparkle, skinTones: nil) + } else if rawValue == "Šī¸" { + self.init(baseEmoji: .copyright, skinTones: nil) + } else if rawValue == "ÂŽī¸" { + self.init(baseEmoji: .registered, skinTones: nil) + } else if rawValue == "â„ĸī¸" { + self.init(baseEmoji: .tm, skinTones: nil) + } else if rawValue == "#ī¸âƒŖ" { + self.init(baseEmoji: .hash, skinTones: nil) + } else if rawValue == "*ī¸âƒŖ" { + self.init(baseEmoji: .keycapStar, skinTones: nil) + } else if rawValue == "0ī¸âƒŖ" { + self.init(baseEmoji: .zero, skinTones: nil) + } else if rawValue == "1ī¸âƒŖ" { + self.init(baseEmoji: .one, skinTones: nil) + } else if rawValue == "2ī¸âƒŖ" { + self.init(baseEmoji: .two, skinTones: nil) + } else if rawValue == "3ī¸âƒŖ" { + self.init(baseEmoji: .three, skinTones: nil) + } else if rawValue == "4ī¸âƒŖ" { + self.init(baseEmoji: .four, skinTones: nil) + } else if rawValue == "5ī¸âƒŖ" { + self.init(baseEmoji: .five, skinTones: nil) + } else if rawValue == "6ī¸âƒŖ" { + self.init(baseEmoji: .six, skinTones: nil) + } else if rawValue == "7ī¸âƒŖ" { + self.init(baseEmoji: .seven, skinTones: nil) + } else if rawValue == "8ī¸âƒŖ" { + self.init(baseEmoji: .eight, skinTones: nil) + } else if rawValue == "9ī¸âƒŖ" { + self.init(baseEmoji: .nine, skinTones: nil) + } else if rawValue == "🔟" { + self.init(baseEmoji: .keycapTen, skinTones: nil) + } else if rawValue == "🔠" { + self.init(baseEmoji: .capitalAbcd, skinTones: nil) + } else if rawValue == "🔡" { + self.init(baseEmoji: .abcd, skinTones: nil) + } else if rawValue == "đŸ”ĸ" { + self.init(baseEmoji: .oneTwoThreeFour, skinTones: nil) + } else if rawValue == "đŸ”Ŗ" { + self.init(baseEmoji: .symbols, skinTones: nil) + } else if rawValue == "🔤" { + self.init(baseEmoji: .abc, skinTones: nil) + } else if rawValue == "🅰ī¸" { + self.init(baseEmoji: .a, skinTones: nil) + } else if rawValue == "🆎" { + self.init(baseEmoji: .ab, skinTones: nil) + } else if rawValue == "🅱ī¸" { + self.init(baseEmoji: .b, skinTones: nil) + } else if rawValue == "🆑" { + self.init(baseEmoji: .cl, skinTones: nil) + } else if rawValue == "🆒" { + self.init(baseEmoji: .cool, skinTones: nil) + } else if rawValue == "🆓" { + self.init(baseEmoji: .free, skinTones: nil) + } else if rawValue == "ℹī¸" { + self.init(baseEmoji: .informationSource, skinTones: nil) + } else if rawValue == "🆔" { + self.init(baseEmoji: .id, skinTones: nil) + } else if rawValue == "Ⓜī¸" { + self.init(baseEmoji: .m, skinTones: nil) + } else if rawValue == "🆕" { + self.init(baseEmoji: .new, skinTones: nil) + } else if rawValue == "🆖" { + self.init(baseEmoji: .ng, skinTones: nil) + } else if rawValue == "🅾ī¸" { + self.init(baseEmoji: .o2, skinTones: nil) + } else if rawValue == "🆗" { + self.init(baseEmoji: .ok, skinTones: nil) + } else if rawValue == "đŸ…ŋī¸" { + self.init(baseEmoji: .parking, skinTones: nil) + } else if rawValue == "🆘" { + self.init(baseEmoji: .sos, skinTones: nil) + } else if rawValue == "🆙" { + self.init(baseEmoji: .up, skinTones: nil) + } else if rawValue == "🆚" { + self.init(baseEmoji: .vs, skinTones: nil) + } else if rawValue == "🈁" { + self.init(baseEmoji: .koko, skinTones: nil) + } else if rawValue == "🈂ī¸" { + self.init(baseEmoji: .sa, skinTones: nil) + } else if rawValue == "🈷ī¸" { + self.init(baseEmoji: .u6708, skinTones: nil) + } else if rawValue == "đŸˆļ" { + self.init(baseEmoji: .u6709, skinTones: nil) + } else if rawValue == "đŸˆ¯" { + self.init(baseEmoji: .u6307, skinTones: nil) + } else if rawValue == "🉐" { + self.init(baseEmoji: .ideographAdvantage, skinTones: nil) + } else if rawValue == "🈹" { + self.init(baseEmoji: .u5272, skinTones: nil) + } else if rawValue == "🈚" { + self.init(baseEmoji: .u7121, skinTones: nil) + } else if rawValue == "🈲" { + self.init(baseEmoji: .u7981, skinTones: nil) + } else if rawValue == "🉑" { + self.init(baseEmoji: .accept, skinTones: nil) + } else if rawValue == "🈸" { + self.init(baseEmoji: .u7533, skinTones: nil) + } else if rawValue == "🈴" { + self.init(baseEmoji: .u5408, skinTones: nil) + } else if rawValue == "đŸˆŗ" { + self.init(baseEmoji: .u7a7a, skinTones: nil) + } else if rawValue == "㊗ī¸" { + self.init(baseEmoji: .congratulations, skinTones: nil) + } else if rawValue == "㊙ī¸" { + self.init(baseEmoji: .secret, skinTones: nil) + } else if rawValue == "đŸˆē" { + self.init(baseEmoji: .u55b6, skinTones: nil) + } else if rawValue == "đŸˆĩ" { + self.init(baseEmoji: .u6e80, skinTones: nil) + } else if rawValue == "🔴" { + self.init(baseEmoji: .redCircle, skinTones: nil) + } else if rawValue == "🟠" { + self.init(baseEmoji: .largeOrangeCircle, skinTones: nil) + } else if rawValue == "🟡" { + self.init(baseEmoji: .largeYellowCircle, skinTones: nil) + } else if rawValue == "đŸŸĸ" { + self.init(baseEmoji: .largeGreenCircle, skinTones: nil) + } else if rawValue == "đŸ”ĩ" { + self.init(baseEmoji: .largeBlueCircle, skinTones: nil) + } else if rawValue == "đŸŸŖ" { + self.init(baseEmoji: .largePurpleCircle, skinTones: nil) + } else if rawValue == "🟤" { + self.init(baseEmoji: .largeBrownCircle, skinTones: nil) + } else if rawValue == "âšĢ" { + self.init(baseEmoji: .blackCircle, skinTones: nil) + } else if rawValue == "âšĒ" { + self.init(baseEmoji: .whiteCircle, skinTones: nil) + } else if rawValue == "đŸŸĨ" { + self.init(baseEmoji: .largeRedSquare, skinTones: nil) + } else if rawValue == "🟧" { + self.init(baseEmoji: .largeOrangeSquare, skinTones: nil) + } else if rawValue == "🟨" { + self.init(baseEmoji: .largeYellowSquare, skinTones: nil) + } else if rawValue == "🟩" { + self.init(baseEmoji: .largeGreenSquare, skinTones: nil) + } else if rawValue == "đŸŸĻ" { + self.init(baseEmoji: .largeBlueSquare, skinTones: nil) + } else if rawValue == "đŸŸĒ" { + self.init(baseEmoji: .largePurpleSquare, skinTones: nil) + } else if rawValue == "đŸŸĢ" { + self.init(baseEmoji: .largeBrownSquare, skinTones: nil) + } else if rawValue == "âŦ›" { + self.init(baseEmoji: .blackLargeSquare, skinTones: nil) + } else if rawValue == "âŦœ" { + self.init(baseEmoji: .whiteLargeSquare, skinTones: nil) + } else if rawValue == "â—ŧī¸" { + self.init(baseEmoji: .blackMediumSquare, skinTones: nil) + } else if rawValue == "â—ģī¸" { + self.init(baseEmoji: .whiteMediumSquare, skinTones: nil) + } else if rawValue == "◾" { + self.init(baseEmoji: .blackMediumSmallSquare, skinTones: nil) + } else if rawValue == "â—Ŋ" { + self.init(baseEmoji: .whiteMediumSmallSquare, skinTones: nil) + } else if rawValue == "â–Ēī¸" { + self.init(baseEmoji: .blackSmallSquare, skinTones: nil) + } else if rawValue == "â–Ģī¸" { + self.init(baseEmoji: .whiteSmallSquare, skinTones: nil) + } else if rawValue == "đŸ”ļ" { + self.init(baseEmoji: .largeOrangeDiamond, skinTones: nil) + } else if rawValue == "🔷" { + self.init(baseEmoji: .largeBlueDiamond, skinTones: nil) + } else if rawValue == "🔸" { + self.init(baseEmoji: .smallOrangeDiamond, skinTones: nil) + } else if rawValue == "🔹" { + self.init(baseEmoji: .smallBlueDiamond, skinTones: nil) + } else if rawValue == "đŸ”ē" { + self.init(baseEmoji: .smallRedTriangle, skinTones: nil) + } else if rawValue == "đŸ”ģ" { + self.init(baseEmoji: .smallRedTriangleDown, skinTones: nil) + } else if rawValue == "💠" { + self.init(baseEmoji: .diamondShapeWithADotInside, skinTones: nil) + } else if rawValue == "🔘" { + self.init(baseEmoji: .radioButton, skinTones: nil) + } else if rawValue == "đŸ”ŗ" { + self.init(baseEmoji: .whiteSquareButton, skinTones: nil) + } else if rawValue == "🔲" { + self.init(baseEmoji: .blackSquareButton, skinTones: nil) + } else if rawValue == "🏁" { + self.init(baseEmoji: .checkeredFlag, skinTones: nil) + } else if rawValue == "🚩" { + self.init(baseEmoji: .triangularFlagOnPost, skinTones: nil) + } else if rawValue == "🎌" { + self.init(baseEmoji: .crossedFlags, skinTones: nil) + } else if rawValue == "🏴" { + self.init(baseEmoji: .wavingBlackFlag, skinTones: nil) + } else if rawValue == "đŸŗī¸" { + self.init(baseEmoji: .wavingWhiteFlag, skinTones: nil) + } else if rawValue == "đŸŗī¸â€đŸŒˆ" { + self.init(baseEmoji: .rainbowFlag, skinTones: nil) + } else if rawValue == "đŸŗī¸â€âš§ī¸" { + self.init(baseEmoji: .transgenderFlag, skinTones: nil) + } else if rawValue == "🏴‍☠ī¸" { + self.init(baseEmoji: .pirateFlag, skinTones: nil) + } else if rawValue == "đŸ‡Ļ🇨" { + self.init(baseEmoji: .flagAc, skinTones: nil) + } else if rawValue == "đŸ‡Ļ🇩" { + self.init(baseEmoji: .flagAd, skinTones: nil) + } else if rawValue == "đŸ‡ĻđŸ‡Ē" { + self.init(baseEmoji: .flagAe, skinTones: nil) + } else if rawValue == "đŸ‡ĻđŸ‡Ģ" { + self.init(baseEmoji: .flagAf, skinTones: nil) + } else if rawValue == "đŸ‡ĻđŸ‡Ŧ" { + self.init(baseEmoji: .flagAg, skinTones: nil) + } else if rawValue == "đŸ‡Ļ🇮" { + self.init(baseEmoji: .flagAi, skinTones: nil) + } else if rawValue == "đŸ‡Ļ🇱" { + self.init(baseEmoji: .flagAl, skinTones: nil) + } else if rawValue == "đŸ‡Ļ🇲" { + self.init(baseEmoji: .flagAm, skinTones: nil) + } else if rawValue == "đŸ‡Ļ🇴" { + self.init(baseEmoji: .flagAo, skinTones: nil) + } else if rawValue == "đŸ‡ĻđŸ‡ļ" { + self.init(baseEmoji: .flagAq, skinTones: nil) + } else if rawValue == "đŸ‡Ļ🇷" { + self.init(baseEmoji: .flagAr, skinTones: nil) + } else if rawValue == "đŸ‡Ļ🇸" { + self.init(baseEmoji: .flagAs, skinTones: nil) + } else if rawValue == "đŸ‡Ļ🇹" { + self.init(baseEmoji: .flagAt, skinTones: nil) + } else if rawValue == "đŸ‡ĻđŸ‡ē" { + self.init(baseEmoji: .flagAu, skinTones: nil) + } else if rawValue == "đŸ‡ĻđŸ‡ŧ" { + self.init(baseEmoji: .flagAw, skinTones: nil) + } else if rawValue == "đŸ‡ĻđŸ‡Ŋ" { + self.init(baseEmoji: .flagAx, skinTones: nil) + } else if rawValue == "đŸ‡ĻđŸ‡ŋ" { + self.init(baseEmoji: .flagAz, skinTones: nil) + } else if rawValue == "🇧đŸ‡Ļ" { + self.init(baseEmoji: .flagBa, skinTones: nil) + } else if rawValue == "🇧🇧" { + self.init(baseEmoji: .flagBb, skinTones: nil) + } else if rawValue == "🇧🇩" { + self.init(baseEmoji: .flagBd, skinTones: nil) + } else if rawValue == "🇧đŸ‡Ē" { + self.init(baseEmoji: .flagBe, skinTones: nil) + } else if rawValue == "🇧đŸ‡Ģ" { + self.init(baseEmoji: .flagBf, skinTones: nil) + } else if rawValue == "🇧đŸ‡Ŧ" { + self.init(baseEmoji: .flagBg, skinTones: nil) + } else if rawValue == "🇧🇭" { + self.init(baseEmoji: .flagBh, skinTones: nil) + } else if rawValue == "🇧🇮" { + self.init(baseEmoji: .flagBi, skinTones: nil) + } else if rawValue == "đŸ‡§đŸ‡¯" { + self.init(baseEmoji: .flagBj, skinTones: nil) + } else if rawValue == "🇧🇱" { + self.init(baseEmoji: .flagBl, skinTones: nil) + } else if rawValue == "🇧🇲" { + self.init(baseEmoji: .flagBm, skinTones: nil) + } else if rawValue == "🇧đŸ‡ŗ" { + self.init(baseEmoji: .flagBn, skinTones: nil) + } else if rawValue == "🇧🇴" { + self.init(baseEmoji: .flagBo, skinTones: nil) + } else if rawValue == "🇧đŸ‡ļ" { + self.init(baseEmoji: .flagBq, skinTones: nil) + } else if rawValue == "🇧🇷" { + self.init(baseEmoji: .flagBr, skinTones: nil) + } else if rawValue == "🇧🇸" { + self.init(baseEmoji: .flagBs, skinTones: nil) + } else if rawValue == "🇧🇹" { + self.init(baseEmoji: .flagBt, skinTones: nil) + } else if rawValue == "🇧đŸ‡ģ" { + self.init(baseEmoji: .flagBv, skinTones: nil) + } else if rawValue == "🇧đŸ‡ŧ" { + self.init(baseEmoji: .flagBw, skinTones: nil) + } else if rawValue == "🇧🇾" { + self.init(baseEmoji: .flagBy, skinTones: nil) + } else if rawValue == "🇧đŸ‡ŋ" { + self.init(baseEmoji: .flagBz, skinTones: nil) + } else if rawValue == "🇨đŸ‡Ļ" { + self.init(baseEmoji: .flagCa, skinTones: nil) + } else if rawValue == "🇨🇨" { + self.init(baseEmoji: .flagCc, skinTones: nil) + } else if rawValue == "🇨🇩" { + self.init(baseEmoji: .flagCd, skinTones: nil) + } else if rawValue == "🇨đŸ‡Ģ" { + self.init(baseEmoji: .flagCf, skinTones: nil) + } else if rawValue == "🇨đŸ‡Ŧ" { + self.init(baseEmoji: .flagCg, skinTones: nil) + } else if rawValue == "🇨🇭" { + self.init(baseEmoji: .flagCh, skinTones: nil) + } else if rawValue == "🇨🇮" { + self.init(baseEmoji: .flagCi, skinTones: nil) + } else if rawValue == "🇨🇰" { + self.init(baseEmoji: .flagCk, skinTones: nil) + } else if rawValue == "🇨🇱" { + self.init(baseEmoji: .flagCl, skinTones: nil) + } else if rawValue == "🇨🇲" { + self.init(baseEmoji: .flagCm, skinTones: nil) + } else if rawValue == "🇨đŸ‡ŗ" { + self.init(baseEmoji: .cn, skinTones: nil) + } else if rawValue == "🇨🇴" { + self.init(baseEmoji: .flagCo, skinTones: nil) + } else if rawValue == "🇨đŸ‡ĩ" { + self.init(baseEmoji: .flagCp, skinTones: nil) + } else if rawValue == "🇨🇷" { + self.init(baseEmoji: .flagCr, skinTones: nil) + } else if rawValue == "🇨đŸ‡ē" { + self.init(baseEmoji: .flagCu, skinTones: nil) + } else if rawValue == "🇨đŸ‡ģ" { + self.init(baseEmoji: .flagCv, skinTones: nil) + } else if rawValue == "🇨đŸ‡ŧ" { + self.init(baseEmoji: .flagCw, skinTones: nil) + } else if rawValue == "🇨đŸ‡Ŋ" { + self.init(baseEmoji: .flagCx, skinTones: nil) + } else if rawValue == "🇨🇾" { + self.init(baseEmoji: .flagCy, skinTones: nil) + } else if rawValue == "🇨đŸ‡ŋ" { + self.init(baseEmoji: .flagCz, skinTones: nil) + } else if rawValue == "🇩đŸ‡Ē" { + self.init(baseEmoji: .de, skinTones: nil) + } else if rawValue == "🇩đŸ‡Ŧ" { + self.init(baseEmoji: .flagDg, skinTones: nil) + } else if rawValue == "đŸ‡ŠđŸ‡¯" { + self.init(baseEmoji: .flagDj, skinTones: nil) + } else if rawValue == "🇩🇰" { + self.init(baseEmoji: .flagDk, skinTones: nil) + } else if rawValue == "🇩🇲" { + self.init(baseEmoji: .flagDm, skinTones: nil) + } else if rawValue == "🇩🇴" { + self.init(baseEmoji: .flagDo, skinTones: nil) + } else if rawValue == "🇩đŸ‡ŋ" { + self.init(baseEmoji: .flagDz, skinTones: nil) + } else if rawValue == "đŸ‡ĒđŸ‡Ļ" { + self.init(baseEmoji: .flagEa, skinTones: nil) + } else if rawValue == "đŸ‡Ē🇨" { + self.init(baseEmoji: .flagEc, skinTones: nil) + } else if rawValue == "đŸ‡ĒđŸ‡Ē" { + self.init(baseEmoji: .flagEe, skinTones: nil) + } else if rawValue == "đŸ‡ĒđŸ‡Ŧ" { + self.init(baseEmoji: .flagEg, skinTones: nil) + } else if rawValue == "đŸ‡Ē🇭" { + self.init(baseEmoji: .flagEh, skinTones: nil) + } else if rawValue == "đŸ‡Ē🇷" { + self.init(baseEmoji: .flagEr, skinTones: nil) + } else if rawValue == "đŸ‡Ē🇸" { + self.init(baseEmoji: .es, skinTones: nil) + } else if rawValue == "đŸ‡Ē🇹" { + self.init(baseEmoji: .flagEt, skinTones: nil) + } else if rawValue == "đŸ‡ĒđŸ‡ē" { + self.init(baseEmoji: .flagEu, skinTones: nil) + } else if rawValue == "đŸ‡Ģ🇮" { + self.init(baseEmoji: .flagFi, skinTones: nil) + } else if rawValue == "đŸ‡ĢđŸ‡¯" { + self.init(baseEmoji: .flagFj, skinTones: nil) + } else if rawValue == "đŸ‡Ģ🇰" { + self.init(baseEmoji: .flagFk, skinTones: nil) + } else if rawValue == "đŸ‡Ģ🇲" { + self.init(baseEmoji: .flagFm, skinTones: nil) + } else if rawValue == "đŸ‡Ģ🇴" { + self.init(baseEmoji: .flagFo, skinTones: nil) + } else if rawValue == "đŸ‡Ģ🇷" { + self.init(baseEmoji: .fr, skinTones: nil) + } else if rawValue == "đŸ‡ŦđŸ‡Ļ" { + self.init(baseEmoji: .flagGa, skinTones: nil) + } else if rawValue == "đŸ‡Ŧ🇧" { + self.init(baseEmoji: .gb, skinTones: nil) + } else if rawValue == "đŸ‡Ŧ🇩" { + self.init(baseEmoji: .flagGd, skinTones: nil) + } else if rawValue == "đŸ‡ŦđŸ‡Ē" { + self.init(baseEmoji: .flagGe, skinTones: nil) + } else if rawValue == "đŸ‡ŦđŸ‡Ģ" { + self.init(baseEmoji: .flagGf, skinTones: nil) + } else if rawValue == "đŸ‡ŦđŸ‡Ŧ" { + self.init(baseEmoji: .flagGg, skinTones: nil) + } else if rawValue == "đŸ‡Ŧ🇭" { + self.init(baseEmoji: .flagGh, skinTones: nil) + } else if rawValue == "đŸ‡Ŧ🇮" { + self.init(baseEmoji: .flagGi, skinTones: nil) + } else if rawValue == "đŸ‡Ŧ🇱" { + self.init(baseEmoji: .flagGl, skinTones: nil) + } else if rawValue == "đŸ‡Ŧ🇲" { + self.init(baseEmoji: .flagGm, skinTones: nil) + } else if rawValue == "đŸ‡ŦđŸ‡ŗ" { + self.init(baseEmoji: .flagGn, skinTones: nil) + } else if rawValue == "đŸ‡ŦđŸ‡ĩ" { + self.init(baseEmoji: .flagGp, skinTones: nil) + } else if rawValue == "đŸ‡ŦđŸ‡ļ" { + self.init(baseEmoji: .flagGq, skinTones: nil) + } else if rawValue == "đŸ‡Ŧ🇷" { + self.init(baseEmoji: .flagGr, skinTones: nil) + } else if rawValue == "đŸ‡Ŧ🇸" { + self.init(baseEmoji: .flagGs, skinTones: nil) + } else if rawValue == "đŸ‡Ŧ🇹" { + self.init(baseEmoji: .flagGt, skinTones: nil) + } else if rawValue == "đŸ‡ŦđŸ‡ē" { + self.init(baseEmoji: .flagGu, skinTones: nil) + } else if rawValue == "đŸ‡ŦđŸ‡ŧ" { + self.init(baseEmoji: .flagGw, skinTones: nil) + } else if rawValue == "đŸ‡Ŧ🇾" { + self.init(baseEmoji: .flagGy, skinTones: nil) + } else if rawValue == "🇭🇰" { + self.init(baseEmoji: .flagHk, skinTones: nil) + } else if rawValue == "🇭🇲" { + self.init(baseEmoji: .flagHm, skinTones: nil) + } else if rawValue == "🇭đŸ‡ŗ" { + self.init(baseEmoji: .flagHn, skinTones: nil) + } else if rawValue == "🇭🇷" { + self.init(baseEmoji: .flagHr, skinTones: nil) + } else if rawValue == "🇭🇹" { + self.init(baseEmoji: .flagHt, skinTones: nil) + } else if rawValue == "🇭đŸ‡ē" { + self.init(baseEmoji: .flagHu, skinTones: nil) + } else if rawValue == "🇮🇨" { + self.init(baseEmoji: .flagIc, skinTones: nil) + } else if rawValue == "🇮🇩" { + self.init(baseEmoji: .flagId, skinTones: nil) + } else if rawValue == "🇮đŸ‡Ē" { + self.init(baseEmoji: .flagIe, skinTones: nil) + } else if rawValue == "🇮🇱" { + self.init(baseEmoji: .flagIl, skinTones: nil) + } else if rawValue == "🇮🇲" { + self.init(baseEmoji: .flagIm, skinTones: nil) + } else if rawValue == "🇮đŸ‡ŗ" { + self.init(baseEmoji: .flagIn, skinTones: nil) + } else if rawValue == "🇮🇴" { + self.init(baseEmoji: .flagIo, skinTones: nil) + } else if rawValue == "🇮đŸ‡ļ" { + self.init(baseEmoji: .flagIq, skinTones: nil) + } else if rawValue == "🇮🇷" { + self.init(baseEmoji: .flagIr, skinTones: nil) + } else if rawValue == "🇮🇸" { + self.init(baseEmoji: .flagIs, skinTones: nil) + } else if rawValue == "🇮🇹" { + self.init(baseEmoji: .it, skinTones: nil) + } else if rawValue == "đŸ‡¯đŸ‡Ē" { + self.init(baseEmoji: .flagJe, skinTones: nil) + } else if rawValue == "đŸ‡¯đŸ‡˛" { + self.init(baseEmoji: .flagJm, skinTones: nil) + } else if rawValue == "đŸ‡¯đŸ‡´" { + self.init(baseEmoji: .flagJo, skinTones: nil) + } else if rawValue == "đŸ‡¯đŸ‡ĩ" { + self.init(baseEmoji: .jp, skinTones: nil) + } else if rawValue == "🇰đŸ‡Ē" { + self.init(baseEmoji: .flagKe, skinTones: nil) + } else if rawValue == "🇰đŸ‡Ŧ" { + self.init(baseEmoji: .flagKg, skinTones: nil) + } else if rawValue == "🇰🇭" { + self.init(baseEmoji: .flagKh, skinTones: nil) + } else if rawValue == "🇰🇮" { + self.init(baseEmoji: .flagKi, skinTones: nil) + } else if rawValue == "🇰🇲" { + self.init(baseEmoji: .flagKm, skinTones: nil) + } else if rawValue == "🇰đŸ‡ŗ" { + self.init(baseEmoji: .flagKn, skinTones: nil) + } else if rawValue == "🇰đŸ‡ĩ" { + self.init(baseEmoji: .flagKp, skinTones: nil) + } else if rawValue == "🇰🇷" { + self.init(baseEmoji: .kr, skinTones: nil) + } else if rawValue == "🇰đŸ‡ŧ" { + self.init(baseEmoji: .flagKw, skinTones: nil) + } else if rawValue == "🇰🇾" { + self.init(baseEmoji: .flagKy, skinTones: nil) + } else if rawValue == "🇰đŸ‡ŋ" { + self.init(baseEmoji: .flagKz, skinTones: nil) + } else if rawValue == "🇱đŸ‡Ļ" { + self.init(baseEmoji: .flagLa, skinTones: nil) + } else if rawValue == "🇱🇧" { + self.init(baseEmoji: .flagLb, skinTones: nil) + } else if rawValue == "🇱🇨" { + self.init(baseEmoji: .flagLc, skinTones: nil) + } else if rawValue == "🇱🇮" { + self.init(baseEmoji: .flagLi, skinTones: nil) + } else if rawValue == "🇱🇰" { + self.init(baseEmoji: .flagLk, skinTones: nil) + } else if rawValue == "🇱🇷" { + self.init(baseEmoji: .flagLr, skinTones: nil) + } else if rawValue == "🇱🇸" { + self.init(baseEmoji: .flagLs, skinTones: nil) + } else if rawValue == "🇱🇹" { + self.init(baseEmoji: .flagLt, skinTones: nil) + } else if rawValue == "🇱đŸ‡ē" { + self.init(baseEmoji: .flagLu, skinTones: nil) + } else if rawValue == "🇱đŸ‡ģ" { + self.init(baseEmoji: .flagLv, skinTones: nil) + } else if rawValue == "🇱🇾" { + self.init(baseEmoji: .flagLy, skinTones: nil) + } else if rawValue == "🇲đŸ‡Ļ" { + self.init(baseEmoji: .flagMa, skinTones: nil) + } else if rawValue == "🇲🇨" { + self.init(baseEmoji: .flagMc, skinTones: nil) + } else if rawValue == "🇲🇩" { + self.init(baseEmoji: .flagMd, skinTones: nil) + } else if rawValue == "🇲đŸ‡Ē" { + self.init(baseEmoji: .flagMe, skinTones: nil) + } else if rawValue == "🇲đŸ‡Ģ" { + self.init(baseEmoji: .flagMf, skinTones: nil) + } else if rawValue == "🇲đŸ‡Ŧ" { + self.init(baseEmoji: .flagMg, skinTones: nil) + } else if rawValue == "🇲🇭" { + self.init(baseEmoji: .flagMh, skinTones: nil) + } else if rawValue == "🇲🇰" { + self.init(baseEmoji: .flagMk, skinTones: nil) + } else if rawValue == "🇲🇱" { + self.init(baseEmoji: .flagMl, skinTones: nil) + } else if rawValue == "🇲🇲" { + self.init(baseEmoji: .flagMm, skinTones: nil) + } else if rawValue == "🇲đŸ‡ŗ" { + self.init(baseEmoji: .flagMn, skinTones: nil) + } else if rawValue == "🇲🇴" { + self.init(baseEmoji: .flagMo, skinTones: nil) + } else if rawValue == "🇲đŸ‡ĩ" { + self.init(baseEmoji: .flagMp, skinTones: nil) + } else if rawValue == "🇲đŸ‡ļ" { + self.init(baseEmoji: .flagMq, skinTones: nil) + } else if rawValue == "🇲🇷" { + self.init(baseEmoji: .flagMr, skinTones: nil) + } else if rawValue == "🇲🇸" { + self.init(baseEmoji: .flagMs, skinTones: nil) + } else if rawValue == "🇲🇹" { + self.init(baseEmoji: .flagMt, skinTones: nil) + } else if rawValue == "🇲đŸ‡ē" { + self.init(baseEmoji: .flagMu, skinTones: nil) + } else if rawValue == "🇲đŸ‡ģ" { + self.init(baseEmoji: .flagMv, skinTones: nil) + } else if rawValue == "🇲đŸ‡ŧ" { + self.init(baseEmoji: .flagMw, skinTones: nil) + } else if rawValue == "🇲đŸ‡Ŋ" { + self.init(baseEmoji: .flagMx, skinTones: nil) + } else if rawValue == "🇲🇾" { + self.init(baseEmoji: .flagMy, skinTones: nil) + } else if rawValue == "🇲đŸ‡ŋ" { + self.init(baseEmoji: .flagMz, skinTones: nil) + } else if rawValue == "đŸ‡ŗđŸ‡Ļ" { + self.init(baseEmoji: .flagNa, skinTones: nil) + } else if rawValue == "đŸ‡ŗ🇨" { + self.init(baseEmoji: .flagNc, skinTones: nil) + } else if rawValue == "đŸ‡ŗđŸ‡Ē" { + self.init(baseEmoji: .flagNe, skinTones: nil) + } else if rawValue == "đŸ‡ŗđŸ‡Ģ" { + self.init(baseEmoji: .flagNf, skinTones: nil) + } else if rawValue == "đŸ‡ŗđŸ‡Ŧ" { + self.init(baseEmoji: .flagNg, skinTones: nil) + } else if rawValue == "đŸ‡ŗ🇮" { + self.init(baseEmoji: .flagNi, skinTones: nil) + } else if rawValue == "đŸ‡ŗ🇱" { + self.init(baseEmoji: .flagNl, skinTones: nil) + } else if rawValue == "đŸ‡ŗ🇴" { + self.init(baseEmoji: .flagNo, skinTones: nil) + } else if rawValue == "đŸ‡ŗđŸ‡ĩ" { + self.init(baseEmoji: .flagNp, skinTones: nil) + } else if rawValue == "đŸ‡ŗ🇷" { + self.init(baseEmoji: .flagNr, skinTones: nil) + } else if rawValue == "đŸ‡ŗđŸ‡ē" { + self.init(baseEmoji: .flagNu, skinTones: nil) + } else if rawValue == "đŸ‡ŗđŸ‡ŋ" { + self.init(baseEmoji: .flagNz, skinTones: nil) + } else if rawValue == "🇴🇲" { + self.init(baseEmoji: .flagOm, skinTones: nil) + } else if rawValue == "đŸ‡ĩđŸ‡Ļ" { + self.init(baseEmoji: .flagPa, skinTones: nil) + } else if rawValue == "đŸ‡ĩđŸ‡Ē" { + self.init(baseEmoji: .flagPe, skinTones: nil) + } else if rawValue == "đŸ‡ĩđŸ‡Ģ" { + self.init(baseEmoji: .flagPf, skinTones: nil) + } else if rawValue == "đŸ‡ĩđŸ‡Ŧ" { + self.init(baseEmoji: .flagPg, skinTones: nil) + } else if rawValue == "đŸ‡ĩ🇭" { + self.init(baseEmoji: .flagPh, skinTones: nil) + } else if rawValue == "đŸ‡ĩ🇰" { + self.init(baseEmoji: .flagPk, skinTones: nil) + } else if rawValue == "đŸ‡ĩ🇱" { + self.init(baseEmoji: .flagPl, skinTones: nil) + } else if rawValue == "đŸ‡ĩ🇲" { + self.init(baseEmoji: .flagPm, skinTones: nil) + } else if rawValue == "đŸ‡ĩđŸ‡ŗ" { + self.init(baseEmoji: .flagPn, skinTones: nil) + } else if rawValue == "đŸ‡ĩ🇷" { + self.init(baseEmoji: .flagPr, skinTones: nil) + } else if rawValue == "đŸ‡ĩ🇸" { + self.init(baseEmoji: .flagPs, skinTones: nil) + } else if rawValue == "đŸ‡ĩ🇹" { + self.init(baseEmoji: .flagPt, skinTones: nil) + } else if rawValue == "đŸ‡ĩđŸ‡ŧ" { + self.init(baseEmoji: .flagPw, skinTones: nil) + } else if rawValue == "đŸ‡ĩ🇾" { + self.init(baseEmoji: .flagPy, skinTones: nil) + } else if rawValue == "đŸ‡ļđŸ‡Ļ" { + self.init(baseEmoji: .flagQa, skinTones: nil) + } else if rawValue == "🇷đŸ‡Ē" { + self.init(baseEmoji: .flagRe, skinTones: nil) + } else if rawValue == "🇷🇴" { + self.init(baseEmoji: .flagRo, skinTones: nil) + } else if rawValue == "🇷🇸" { + self.init(baseEmoji: .flagRs, skinTones: nil) + } else if rawValue == "🇷đŸ‡ē" { + self.init(baseEmoji: .ru, skinTones: nil) + } else if rawValue == "🇷đŸ‡ŧ" { + self.init(baseEmoji: .flagRw, skinTones: nil) + } else if rawValue == "🇸đŸ‡Ļ" { + self.init(baseEmoji: .flagSa, skinTones: nil) + } else if rawValue == "🇸🇧" { + self.init(baseEmoji: .flagSb, skinTones: nil) + } else if rawValue == "🇸🇨" { + self.init(baseEmoji: .flagSc, skinTones: nil) + } else if rawValue == "🇸🇩" { + self.init(baseEmoji: .flagSd, skinTones: nil) + } else if rawValue == "🇸đŸ‡Ē" { + self.init(baseEmoji: .flagSe, skinTones: nil) + } else if rawValue == "🇸đŸ‡Ŧ" { + self.init(baseEmoji: .flagSg, skinTones: nil) + } else if rawValue == "🇸🇭" { + self.init(baseEmoji: .flagSh, skinTones: nil) + } else if rawValue == "🇸🇮" { + self.init(baseEmoji: .flagSi, skinTones: nil) + } else if rawValue == "đŸ‡¸đŸ‡¯" { + self.init(baseEmoji: .flagSj, skinTones: nil) + } else if rawValue == "🇸🇰" { + self.init(baseEmoji: .flagSk, skinTones: nil) + } else if rawValue == "🇸🇱" { + self.init(baseEmoji: .flagSl, skinTones: nil) + } else if rawValue == "🇸🇲" { + self.init(baseEmoji: .flagSm, skinTones: nil) + } else if rawValue == "🇸đŸ‡ŗ" { + self.init(baseEmoji: .flagSn, skinTones: nil) + } else if rawValue == "🇸🇴" { + self.init(baseEmoji: .flagSo, skinTones: nil) + } else if rawValue == "🇸🇷" { + self.init(baseEmoji: .flagSr, skinTones: nil) + } else if rawValue == "🇸🇸" { + self.init(baseEmoji: .flagSs, skinTones: nil) + } else if rawValue == "🇸🇹" { + self.init(baseEmoji: .flagSt, skinTones: nil) + } else if rawValue == "🇸đŸ‡ģ" { + self.init(baseEmoji: .flagSv, skinTones: nil) + } else if rawValue == "🇸đŸ‡Ŋ" { + self.init(baseEmoji: .flagSx, skinTones: nil) + } else if rawValue == "🇸🇾" { + self.init(baseEmoji: .flagSy, skinTones: nil) + } else if rawValue == "🇸đŸ‡ŋ" { + self.init(baseEmoji: .flagSz, skinTones: nil) + } else if rawValue == "🇹đŸ‡Ļ" { + self.init(baseEmoji: .flagTa, skinTones: nil) + } else if rawValue == "🇹🇨" { + self.init(baseEmoji: .flagTc, skinTones: nil) + } else if rawValue == "🇹🇩" { + self.init(baseEmoji: .flagTd, skinTones: nil) + } else if rawValue == "🇹đŸ‡Ģ" { + self.init(baseEmoji: .flagTf, skinTones: nil) + } else if rawValue == "🇹đŸ‡Ŧ" { + self.init(baseEmoji: .flagTg, skinTones: nil) + } else if rawValue == "🇹🇭" { + self.init(baseEmoji: .flagTh, skinTones: nil) + } else if rawValue == "đŸ‡šđŸ‡¯" { + self.init(baseEmoji: .flagTj, skinTones: nil) + } else if rawValue == "🇹🇰" { + self.init(baseEmoji: .flagTk, skinTones: nil) + } else if rawValue == "🇹🇱" { + self.init(baseEmoji: .flagTl, skinTones: nil) + } else if rawValue == "🇹🇲" { + self.init(baseEmoji: .flagTm, skinTones: nil) + } else if rawValue == "🇹đŸ‡ŗ" { + self.init(baseEmoji: .flagTn, skinTones: nil) + } else if rawValue == "🇹🇴" { + self.init(baseEmoji: .flagTo, skinTones: nil) + } else if rawValue == "🇹🇷" { + self.init(baseEmoji: .flagTr, skinTones: nil) + } else if rawValue == "🇹🇹" { + self.init(baseEmoji: .flagTt, skinTones: nil) + } else if rawValue == "🇹đŸ‡ģ" { + self.init(baseEmoji: .flagTv, skinTones: nil) + } else if rawValue == "🇹đŸ‡ŧ" { + self.init(baseEmoji: .flagTw, skinTones: nil) + } else if rawValue == "🇹đŸ‡ŋ" { + self.init(baseEmoji: .flagTz, skinTones: nil) + } else if rawValue == "đŸ‡ēđŸ‡Ļ" { + self.init(baseEmoji: .flagUa, skinTones: nil) + } else if rawValue == "đŸ‡ēđŸ‡Ŧ" { + self.init(baseEmoji: .flagUg, skinTones: nil) + } else if rawValue == "đŸ‡ē🇲" { + self.init(baseEmoji: .flagUm, skinTones: nil) + } else if rawValue == "đŸ‡ēđŸ‡ŗ" { + self.init(baseEmoji: .flagUn, skinTones: nil) + } else if rawValue == "đŸ‡ē🇸" { + self.init(baseEmoji: .us, skinTones: nil) + } else if rawValue == "đŸ‡ē🇾" { + self.init(baseEmoji: .flagUy, skinTones: nil) + } else if rawValue == "đŸ‡ēđŸ‡ŋ" { + self.init(baseEmoji: .flagUz, skinTones: nil) + } else if rawValue == "đŸ‡ģđŸ‡Ļ" { + self.init(baseEmoji: .flagVa, skinTones: nil) + } else if rawValue == "đŸ‡ģ🇨" { + self.init(baseEmoji: .flagVc, skinTones: nil) + } else if rawValue == "đŸ‡ģđŸ‡Ē" { + self.init(baseEmoji: .flagVe, skinTones: nil) + } else if rawValue == "đŸ‡ģđŸ‡Ŧ" { + self.init(baseEmoji: .flagVg, skinTones: nil) + } else if rawValue == "đŸ‡ģ🇮" { + self.init(baseEmoji: .flagVi, skinTones: nil) + } else if rawValue == "đŸ‡ģđŸ‡ŗ" { + self.init(baseEmoji: .flagVn, skinTones: nil) + } else if rawValue == "đŸ‡ģđŸ‡ē" { + self.init(baseEmoji: .flagVu, skinTones: nil) + } else if rawValue == "đŸ‡ŧđŸ‡Ģ" { + self.init(baseEmoji: .flagWf, skinTones: nil) + } else if rawValue == "đŸ‡ŧ🇸" { + self.init(baseEmoji: .flagWs, skinTones: nil) + } else if rawValue == "đŸ‡Ŋ🇰" { + self.init(baseEmoji: .flagXk, skinTones: nil) + } else if rawValue == "🇾đŸ‡Ē" { + self.init(baseEmoji: .flagYe, skinTones: nil) + } else if rawValue == "🇾🇹" { + self.init(baseEmoji: .flagYt, skinTones: nil) + } else if rawValue == "đŸ‡ŋđŸ‡Ļ" { + self.init(baseEmoji: .flagZa, skinTones: nil) + } else if rawValue == "đŸ‡ŋ🇲" { + self.init(baseEmoji: .flagZm, skinTones: nil) + } else if rawValue == "đŸ‡ŋđŸ‡ŧ" { + self.init(baseEmoji: .flagZw, skinTones: nil) + } else if rawValue == "🏴ķ §ķ ĸķ Ĩķ Žķ §ķ ŋ" { + self.init(baseEmoji: .flagEngland, skinTones: nil) + } else if rawValue == "🏴ķ §ķ ĸķ ŗķ Ŗķ ´ķ ŋ" { + self.init(baseEmoji: .flagScotland, skinTones: nil) + } else if rawValue == "🏴ķ §ķ ĸķ ˇķ Ŧķ ŗķ ŋ" { + self.init(baseEmoji: .flagWales, skinTones: nil) + } else { + self.init(unsupportedValue: rawValue) + } + } +} diff --git a/Session/Emoji/EmojiWithSkinTones.swift b/Session/Emoji/EmojiWithSkinTones.swift new file mode 100644 index 000000000..293a9c9c2 --- /dev/null +++ b/Session/Emoji/EmojiWithSkinTones.swift @@ -0,0 +1,138 @@ +// 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 = ["😂", "đŸĨ°", "đŸ˜ĸ", "😡", "😮", "😈"] + .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 + } +} diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index f45a9ece4..2f00bc37c 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -214,9 +214,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve } // Onion request path countries cache - DispatchQueue.global(qos: .utility).sync { - let _ = IP2Country.shared.populateCacheIfNeeded() - } + IP2Country.shared.populateCacheIfNeededAsync() } override func viewWillAppear(_ animated: Bool) { diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index a6c99443b..e343e3ea1 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -43,8 +43,7 @@ public class HomeViewModel { // MARK: - Initialization init() { - self.state = Storage.shared.read { db in try HomeViewModel.retrieveState(db) } - .defaulting(to: State()) + self.state = State() self.pagedDataObserver = nil // Note: Since this references self we need to finish initializing before setting it, we @@ -135,18 +134,18 @@ public class HomeViewModel { joinToPagedType: { let typingIndicator: TypedTableAlias = TypedTableAlias() - return SQL("LEFT JOIN \(typingIndicator[.threadId]) = \(thread[.id])") + return SQL("LEFT JOIN \(ThreadTypingIndicator.self) ON \(typingIndicator[.threadId]) = \(thread[.id])") }() ) ], - /// **Note:** This `optimisedJoinSQL` value includes the required minimum joins needed for the query + /// **Note:** This `optimisedJoinSQL` value includes the required minimum joins needed for the query but differs + /// from the JOINs that are actually used for performance reasons as the basic logic can be simpler for where it's used joinSQL: SessionThreadViewModel.optimisedJoinSQL, filterSQL: SessionThreadViewModel.homeFilterSQL(userPublicKey: userPublicKey), groupSQL: SessionThreadViewModel.groupSQL, orderSQL: SessionThreadViewModel.homeOrderSQL, dataQuery: SessionThreadViewModel.baseQuery( userPublicKey: userPublicKey, - filterSQL: SessionThreadViewModel.homeFilterSQL(userPublicKey: userPublicKey), groupSQL: SessionThreadViewModel.groupSQL, orderSQL: SessionThreadViewModel.homeOrderSQL ), @@ -194,8 +193,9 @@ public class HomeViewModel { let hasHiddenMessageRequests: Bool = db[.hasHiddenMessageRequests] let userProfile: Profile = Profile.fetchOrCreateCurrentUser(db) let unreadMessageRequestThreadCount: Int = try SessionThread - .unreadMessageRequestsThreadIdQuery(userPublicKey: userProfile.id) - .fetchCount(db) + .unreadMessageRequestsCountQuery(userPublicKey: userProfile.id) + .fetchOne(db) + .defaulting(to: 0) return State( showViewedSeedBanner: !hasViewedSeed, @@ -219,7 +219,8 @@ public class HomeViewModel { else { return } /// **MUST** have the same logic as in the 'PagedDataObserver.onChangeUnsorted' above - let currentData: [SessionThreadViewModel] = self.threadData.flatMap { $0.elements } + let currentData: [SessionThreadViewModel] = (self.unobservedThreadDataChanges ?? self.threadData) + .flatMap { $0.elements } let updatedThreadData: [SectionModel] = self.process(data: currentData, for: currentPageInfo) guard let onThreadChange: (([SectionModel]) -> ()) = self.onThreadChange else { diff --git a/Session/Home/Message Requests/MessageRequestsViewModel.swift b/Session/Home/Message Requests/MessageRequestsViewModel.swift index c19ff8538..e3dadb79c 100644 --- a/Session/Home/Message Requests/MessageRequestsViewModel.swift +++ b/Session/Home/Message Requests/MessageRequestsViewModel.swift @@ -86,14 +86,14 @@ public class MessageRequestsViewModel { }() ) ], - /// **Note:** This `optimisedJoinSQL` value includes the required minimum joins needed for the query + /// **Note:** This `optimisedJoinSQL` value includes the required minimum joins needed for the query but differs + /// from the JOINs that are actually used for performance reasons as the basic logic can be simpler for where it's used joinSQL: SessionThreadViewModel.optimisedJoinSQL, filterSQL: SessionThreadViewModel.messageRequestsFilterSQL(userPublicKey: userPublicKey), groupSQL: SessionThreadViewModel.groupSQL, orderSQL: SessionThreadViewModel.messageRequetsOrderSQL, dataQuery: SessionThreadViewModel.baseQuery( userPublicKey: userPublicKey, - filterSQL: SessionThreadViewModel.messageRequestsFilterSQL(userPublicKey: userPublicKey), groupSQL: SessionThreadViewModel.groupSQL, orderSQL: SessionThreadViewModel.messageRequetsOrderSQL ), diff --git a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift index cc425d2d3..bad7fc350 100644 --- a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift +++ b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift @@ -367,7 +367,7 @@ public class MediaGalleryViewModel { .removeDuplicates() } - @discardableResult public func loadAndCacheAlbumData(for interactionId: Int64) -> [Item] { + @discardableResult public func loadAndCacheAlbumData(for interactionId: Int64, in threadId: String) -> [Item] { typealias AlbumInfo = (albumData: [Item], interactionIdBefore: Int64?, interactionIdAfter: Int64?) // Note: It's possible we already have cached album data for this interaction @@ -394,13 +394,19 @@ public class MediaGalleryViewModel { let itemBefore: Item? = try Item .baseQuery( orderSQL: Item.galleryReverseOrderSQL, - customFilters: SQL("\(interaction[.timestampMs]) > \(albumTimestampMs)") + customFilters: SQL(""" + \(interaction[.timestampMs]) > \(albumTimestampMs) AND + \(interaction[.threadId]) = \(threadId) + """) ) .fetchOne(db) let itemAfter: Item? = try Item .baseQuery( orderSQL: Item.galleryOrderSQL, - customFilters: SQL("\(interaction[.timestampMs]) < \(albumTimestampMs)") + customFilters: SQL(""" + \(interaction[.timestampMs]) < \(albumTimestampMs) AND + \(interaction[.threadId]) = \(threadId) + """) ) .fetchOne(db) @@ -505,7 +511,7 @@ public class MediaGalleryViewModel { threadVariant: threadVariant, isPagedData: false ) - viewModel.loadAndCacheAlbumData(for: interactionId) + viewModel.loadAndCacheAlbumData(for: interactionId, in: threadId) viewModel.replaceAlbumObservation(toObservationFor: interactionId) guard diff --git a/Session/Media Viewing & Editing/MediaPageViewController.swift b/Session/Media Viewing & Editing/MediaPageViewController.swift index 7b9f96349..ec290ea7e 100644 --- a/Session/Media Viewing & Editing/MediaPageViewController.swift +++ b/Session/Media Viewing & Editing/MediaPageViewController.swift @@ -681,10 +681,15 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou } // Then check if there is an interaction before the current album interaction - guard let interactionIdAfter: Int64 = self.viewModel.interactionIdAfter[interactionId] else { return nil } + guard let interactionIdAfter: Int64 = self.viewModel.interactionIdAfter[interactionId] else { + return nil + } // Cache and retrieve the new album items - let newAlbumItems: [MediaGalleryViewModel.Item] = viewModel.loadAndCacheAlbumData(for: interactionIdAfter) + let newAlbumItems: [MediaGalleryViewModel.Item] = viewModel.loadAndCacheAlbumData( + for: interactionIdAfter, + in: self.viewModel.threadId + ) guard !newAlbumItems.isEmpty, @@ -723,10 +728,15 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou } // Then check if there is an interaction before the current album interaction - guard let interactionIdBefore: Int64 = self.viewModel.interactionIdBefore[interactionId] else { return nil } + guard let interactionIdBefore: Int64 = self.viewModel.interactionIdBefore[interactionId] else { + return nil + } // Cache and retrieve the new album items - let newAlbumItems: [MediaGalleryViewModel.Item] = viewModel.loadAndCacheAlbumData(for: interactionIdBefore) + let newAlbumItems: [MediaGalleryViewModel.Item] = viewModel.loadAndCacheAlbumData( + for: interactionIdBefore, + in: self.viewModel.threadId + ) guard !newAlbumItems.isEmpty, diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 3bbff0213..79db24db8 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -133,10 +133,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // NOTE: Fix an edge case where user taps on the callkit notification // but answers the call on another device stopPollers(shouldStopUserPoller: !self.hasIncomingCallWaiting()) - JobRunner.stopAndClearPendingJobs() - // Suspend database - NotificationCenter.default.post(name: Database.suspendNotification, object: self) + // Stop all jobs except for message sending and when completed suspend the database + JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend) { + NotificationCenter.default.post(name: Database.suspendNotification, object: self) + } } func applicationDidReceiveMemoryWarning(_ application: UIApplication) { diff --git a/Session/Meta/Signal-Bridging-Header.h b/Session/Meta/Signal-Bridging-Header.h index c9b488573..bd11856ea 100644 --- a/Session/Meta/Signal-Bridging-Header.h +++ b/Session/Meta/Signal-Bridging-Header.h @@ -44,7 +44,6 @@ #import #import #import -#import #import #import #import diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index d12868e54..415dd41d3 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 6dd37a8bb..64e3a9c83 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index e5d4f9d44..0de94349d 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index f333f1b70..8a310885b 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index b9e9b8267..bea2c1277 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index f99011932..345cbc2e6 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index eca955776..c9298f93c 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index bb4556500..324a0702d 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 6322bf9f2..27c64084d 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index eedf2137b..2ae8ccb16 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 253f72de8..a0f7b3120 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index d58ebff00..58c2486c5 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index 5a08f7e35..4a809aa69 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index a848b7587..2aacfc5d1 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index a3984387e..5d2070325 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index cacf3f5d1..12553bb60 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index 102514ede..6f6cff3ca 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index da8933ad7..d47e96467 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index b8f6e41ac..6a9d0aae9 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index fd93b1f63..aff668e23 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index f936369d4..2125e6eea 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 9145c3fe8..0352a3a29 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -684,3 +684,24 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +/* The name for the emoji category 'Activities' */ +"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; +/* The name for the emoji category 'Animals & Nature' */ +"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature"; +/* The name for the emoji category 'Flags' */ +"EMOJI_CATEGORY_FLAGS_NAME" = "Flags"; +/* The name for the emoji category 'Food & Drink' */ +"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink"; +/* The name for the emoji category 'Objects' */ +"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects"; +/* The name for the emoji category 'Recents' */ +"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used"; +/* The name for the emoji category 'Smileys & People' */ +"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People"; +/* The name for the emoji category 'Symbols' */ +"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols"; +/* The name for the emoji category 'Travel & Places' */ +"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places"; +"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index e894f5ad6..e0c25749b 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -82,10 +82,6 @@ extension AppNotificationAction { } } -// Delay notification of incoming messages when it's a background polling to -// avoid too many notifications fired at the same time -let kNotificationDelayForBackgroumdPoll: TimeInterval = 5 - let kAudioNotificationsThrottleCount = 2 let kAudioNotificationsThrottleInterval: TimeInterval = 5 @@ -93,14 +89,48 @@ protocol NotificationPresenterAdaptee: AnyObject { func registerNotificationSettings() -> Promise - func notify(category: AppNotificationCategory, title: String?, body: String, userInfo: [AnyHashable: Any], sound: Preferences.Sound?) - func notify(category: AppNotificationCategory, title: String?, body: String, userInfo: [AnyHashable: Any], sound: Preferences.Sound?, replacingIdentifier: String?) + func notify( + category: AppNotificationCategory, + title: String?, + body: String, + userInfo: [AnyHashable: Any], + previewType: Preferences.NotificationPreviewType, + sound: Preferences.Sound?, + threadVariant: SessionThread.Variant, + threadName: String, + replacingIdentifier: String? + ) func cancelNotifications(threadId: String) func cancelNotifications(identifiers: [String]) func clearAllNotifications() } +extension NotificationPresenterAdaptee { + func notify( + category: AppNotificationCategory, + title: String?, + body: String, + userInfo: [AnyHashable: Any], + previewType: Preferences.NotificationPreviewType, + sound: Preferences.Sound?, + threadVariant: SessionThread.Variant, + threadName: String + ) { + notify( + category: category, + title: title, + body: body, + userInfo: userInfo, + previewType: previewType, + sound: sound, + threadVariant: threadVariant, + threadName: threadName, + replacingIdentifier: nil + ) + } +} + @objc(OWSNotificationPresenter) public class NotificationPresenter: NSObject, NotificationsProtocol { @@ -141,7 +171,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { return adaptee.registerNotificationSettings() } - public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, isBackgroundPoll: Bool) { + public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread) { let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true) // Ensure we should be showing a notification for the thread @@ -149,7 +179,10 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { return } - let identifier: String = interaction.notificationIdentifier(isBackgroundPoll: isBackgroundPoll) + // Try to group notifications for interactions from open groups + let identifier: String = interaction.notificationIdentifier( + shouldGroupMessagesForThread: (thread.variant == .openGroup) + ) // While batch processing, some of the necessary changes have not been commited. let rawMessageText = interaction.previewText(db) @@ -166,6 +199,18 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { let senderName = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant) let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType] .defaulting(to: .nameAndPreview) + let groupName: String = SessionThread.displayName( + threadId: thread.id, + variant: thread.variant, + closedGroupName: try? thread.closedGroup + .select(.name) + .asRequest(of: String.self) + .fetchOne(db), + openGroupName: try? thread.openGroup + .select(.name) + .asRequest(of: String.self) + .fetchOne(db) + ) switch previewType { case .noNameNoPreview: @@ -177,26 +222,10 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { notificationTitle = (isMessageRequest ? "Session" : senderName) case .closedGroup, .openGroup: - let groupName: String = SessionThread - .displayName( - threadId: thread.id, - variant: thread.variant, - closedGroupName: try? thread.closedGroup - .select(.name) - .asRequest(of: String.self) - .fetchOne(db), - openGroupName: try? thread.openGroup - .select(.name) - .asRequest(of: String.self) - .fetchOne(db) - ) - - notificationTitle = (isBackgroundPoll ? groupName : - String( - format: NotificationStrings.incomingGroupMessageTitleFormat, - senderName, - groupName - ) + notificationTitle = String( + format: NotificationStrings.incomingGroupMessageTitleFormat, + senderName, + groupName ) } } @@ -230,22 +259,31 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { threadId: thread.id, threadVariant: thread.variant ) + let fallbackSound: Preferences.Sound = db[.defaultNotificationSound] + .defaulting(to: Preferences.Sound.defaultNotificationSound) DispatchQueue.main.async { + let sound: Preferences.Sound? = self.requestSound( + thread: thread, + fallbackSound: fallbackSound + ) + notificationBody = MentionUtilities.highlightMentions( in: (notificationBody ?? ""), threadVariant: thread.variant, currentUserPublicKey: userPublicKey, currentUserBlindedPublicKey: userBlindedKey ) - let sound: Preferences.Sound? = self.requestSound(thread: thread) self.adaptee.notify( category: category, title: notificationTitle, - body: notificationBody ?? "", + body: (notificationBody ?? ""), userInfo: userInfo, + previewType: previewType, sound: sound, + threadVariant: thread.variant, + threadName: groupName, replacingIdentifier: identifier ) } @@ -268,35 +306,102 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { guard messageInfo.state == .missed || messageInfo.state == .permissionDenied else { return } let category = AppNotificationCategory.errorMessage - + let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType] + .defaulting(to: .nameAndPreview) + let userInfo = [ AppNotificationUserInfoKey.threadId: thread.id ] - let notificationTitle = interaction.previewText(db) + let notificationTitle: String = interaction.previewText(db) + let threadName: String = SessionThread.displayName( + threadId: thread.id, + variant: thread.variant, + closedGroupName: nil, // Not supported + openGroupName: nil // Not supported + ) var notificationBody: String? if messageInfo.state == .permissionDenied { notificationBody = String( format: "modal_call_missed_tips_explanation".localized(), - SessionThread.displayName( - threadId: thread.id, - variant: thread.variant, - closedGroupName: nil, // Not supported - openGroupName: nil // Not supported - ) + threadName ) } + let fallbackSound: Preferences.Sound = db[.defaultNotificationSound] + .defaulting(to: Preferences.Sound.defaultNotificationSound) DispatchQueue.main.async { - let sound = self.requestSound(thread: thread) + let sound = self.requestSound( + thread: thread, + fallbackSound: fallbackSound + ) self.adaptee.notify( category: category, title: notificationTitle, - body: notificationBody ?? "", + body: (notificationBody ?? ""), userInfo: userInfo, + previewType: previewType, sound: sound, + threadVariant: thread.variant, + threadName: threadName, + replacingIdentifier: UUID().uuidString + ) + } + } + + public func notifyUser(_ db: Database, forReaction reaction: Reaction, in thread: SessionThread) { + let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true) + + // No reaction notifications for muted, group threads or message requests + guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return } + guard thread.variant != .closedGroup && thread.variant != .openGroup else { return } + guard !isMessageRequest else { return } + + let senderName: String = Profile.displayName(db, id: reaction.authorId, threadVariant: thread.variant) + let notificationTitle = "Session" + var notificationBody = String(format: "EMOJI_REACTS_NOTIFICATION".localized(), senderName, reaction.emoji) + + // Title & body + let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType] + .defaulting(to: .nameAndPreview) + + switch previewType { + case .nameAndPreview: break + default: notificationBody = NotificationStrings.incomingMessageBody + } + + let category = AppNotificationCategory.incomingMessage + + let userInfo = [ + AppNotificationUserInfoKey.threadId: thread.id + ] + + let threadName: String = SessionThread.displayName( + threadId: thread.id, + variant: thread.variant, + closedGroupName: nil, // Not supported + openGroupName: nil // Not supported + ) + let fallbackSound: Preferences.Sound = db[.defaultNotificationSound] + .defaulting(to: Preferences.Sound.defaultNotificationSound) + + DispatchQueue.main.async { + let sound = self.requestSound( + thread: thread, + fallbackSound: fallbackSound + ) + + self.adaptee.notify( + category: category, + title: notificationTitle, + body: notificationBody, + userInfo: userInfo, + previewType: previewType, + sound: sound, + threadVariant: thread.variant, + threadName: threadName, replacingIdentifier: UUID().uuidString ) } @@ -306,24 +411,24 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { let notificationTitle: String? let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType] .defaulting(to: .nameAndPreview) + let threadName: String = SessionThread.displayName( + threadId: thread.id, + variant: thread.variant, + closedGroupName: try? thread.closedGroup + .select(.name) + .asRequest(of: String.self) + .fetchOne(db), + openGroupName: try? thread.openGroup + .select(.name) + .asRequest(of: String.self) + .fetchOne(db), + isNoteToSelf: (thread.isNoteToSelf(db) == true), + profile: try? Profile.fetchOne(db, id: thread.id) + ) switch previewType { case .noNameNoPreview: notificationTitle = nil - case .nameNoPreview, .nameAndPreview: - notificationTitle = SessionThread.displayName( - threadId: thread.id, - variant: thread.variant, - closedGroupName: try? thread.closedGroup - .select(.name) - .asRequest(of: String.self) - .fetchOne(db), - openGroupName: try? thread.openGroup - .select(.name) - .asRequest(of: String.self) - .fetchOne(db), - isNoteToSelf: (thread.isNoteToSelf(db) == true), - profile: try? Profile.fetchOne(db, id: thread.id) - ) + case .nameNoPreview, .nameAndPreview: notificationTitle = threadName } let notificationBody = NotificationStrings.failedToSendBody @@ -331,16 +436,24 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { let userInfo = [ AppNotificationUserInfoKey.threadId: thread.id ] + let fallbackSound: Preferences.Sound = db[.defaultNotificationSound] + .defaulting(to: Preferences.Sound.defaultNotificationSound) DispatchQueue.main.async { - let sound: Preferences.Sound? = self.requestSound(thread: thread) + let sound: Preferences.Sound? = self.requestSound( + thread: thread, + fallbackSound: fallbackSound + ) self.adaptee.notify( category: .errorMessage, title: notificationTitle, body: notificationBody, userInfo: userInfo, - sound: sound + previewType: previewType, + sound: sound, + threadVariant: thread.variant, + threadName: threadName ) } } @@ -366,12 +479,12 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { var mostRecentNotifications = TruncatedList(maxLength: kAudioNotificationsThrottleCount) - private func requestSound(thread: SessionThread) -> Preferences.Sound? { + private func requestSound(thread: SessionThread, fallbackSound: Preferences.Sound) -> Preferences.Sound? { guard checkIfShouldPlaySound() else { return nil } - - return thread.notificationSound + + return (thread.notificationSound ?? fallbackSound) } private func checkIfShouldPlaySound() -> Bool { diff --git a/Session/Notifications/UserNotificationsAdaptee.swift b/Session/Notifications/UserNotificationsAdaptee.swift index c7fd88113..c6da6323b 100644 --- a/Session/Notifications/UserNotificationsAdaptee.swift +++ b/Session/Notifications/UserNotificationsAdaptee.swift @@ -57,8 +57,9 @@ class UserNotificationPresenterAdaptee: NSObject, UNUserNotificationCenterDelega override init() { self.notificationCenter = UNUserNotificationCenter.current() + super.init() - notificationCenter.delegate = self + SwiftSingletons.register(self) } } @@ -86,29 +87,37 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee { } } - func notify(category: AppNotificationCategory, title: String?, body: String, userInfo: [AnyHashable: Any], sound: Preferences.Sound?) { - AssertIsOnMainThread() - notify(category: category, title: title, body: body, userInfo: userInfo, sound: sound, replacingIdentifier: nil) - } - - func notify(category: AppNotificationCategory, title: String?, body: String, userInfo: [AnyHashable: Any], sound: Preferences.Sound?, replacingIdentifier: String?) { + func notify( + category: AppNotificationCategory, + title: String?, + body: String, + userInfo: [AnyHashable: Any], + previewType: Preferences.NotificationPreviewType, + sound: Preferences.Sound?, + threadVariant: SessionThread.Variant, + threadName: String, + replacingIdentifier: String? + ) { AssertIsOnMainThread() + let threadIdentifier: String? = (userInfo[AppNotificationUserInfoKey.threadId] as? String) let content = UNMutableNotificationContent() content.categoryIdentifier = category.identifier content.userInfo = userInfo - let isReplacingNotification = replacingIdentifier != nil - var isBackgroudPoll = false - if let threadIdentifier = userInfo[AppNotificationUserInfoKey.threadId] as? String { - content.threadIdentifier = threadIdentifier - isBackgroudPoll = replacingIdentifier == threadIdentifier - } + content.threadIdentifier = (threadIdentifier ?? content.threadIdentifier) + + let shouldGroupNotification: Bool = ( + threadVariant == .openGroup && + replacingIdentifier == threadIdentifier + ) let isAppActive = UIApplication.shared.applicationState == .active if let sound = sound, sound != .none { content.sound = sound.notificationSound(isQuiet: isAppActive) } - let notificationIdentifier = isReplacingNotification ? replacingIdentifier! : UUID().uuidString + let notificationIdentifier: String = (replacingIdentifier ?? UUID().uuidString) + let isReplacingNotification: Bool = (notifications[notificationIdentifier] != nil) + var trigger: UNNotificationTrigger? if shouldPresentNotification(category: category, userInfo: userInfo) { if let displayableTitle = title?.filterForDisplay { @@ -117,30 +126,50 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee { if let displayableBody = body.filterForDisplay { content.body = displayableBody } - } else { + + if shouldGroupNotification { + trigger = UNTimeIntervalNotificationTrigger( + timeInterval: Notifications.delayForGroupedNotifications, + repeats: false + ) + + let numberExistingNotifications: Int? = notifications[notificationIdentifier]? + .content + .userInfo[AppNotificationUserInfoKey.threadNotificationCounter] + .asType(Int.self) + var numberOfNotifications: Int = (numberExistingNotifications ?? 1) + + if numberExistingNotifications != nil { + numberOfNotifications += 1 // Add one for the current notification + + content.title = (previewType == .noNameNoPreview ? + content.title : + threadName + ) + content.body = String( + format: NotificationStrings.incomingCollapsedMessagesBody, + "\(numberOfNotifications)" + ) + } + + content.userInfo[AppNotificationUserInfoKey.threadNotificationCounter] = numberOfNotifications + } + } + else { // Play sound and vibrate, but without a `body` no banner will show. Logger.debug("supressing notification body") } - - let trigger: UNNotificationTrigger? - if isBackgroudPoll { - trigger = UNTimeIntervalNotificationTrigger(timeInterval: kNotificationDelayForBackgroumdPoll, repeats: false) - let numberOfNotifications: Int - if let lastRequest = notifications[notificationIdentifier], let counter = lastRequest.content.userInfo[AppNotificationUserInfoKey.threadNotificationCounter] as? Int { - numberOfNotifications = counter + 1 - content.body = String(format: NotificationStrings.incomingCollapsedMessagesBody, "\(numberOfNotifications)") - } else { - numberOfNotifications = 1 - } - content.userInfo[AppNotificationUserInfoKey.threadNotificationCounter] = numberOfNotifications - } else { - trigger = nil - } - let request = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: trigger) + let request = UNNotificationRequest( + identifier: notificationIdentifier, + content: content, + trigger: trigger + ) Logger.debug("presenting notification with identifier: \(notificationIdentifier)") + if isReplacingNotification { cancelNotifications(identifiers: [notificationIdentifier]) } + notificationCenter.add(request) notifications[notificationIdentifier] = request } @@ -196,7 +225,7 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee { guard let conversationViewController = UIApplication.shared.frontmostViewController as? ConversationVC else { return true } - + /// Show notifications for any **other** threads return (conversationViewController.viewModel.threadData.threadId != notificationThreadId) } diff --git a/Session/Onboarding/LandingVC.swift b/Session/Onboarding/LandingVC.swift index 2a34fff7d..0bfd0c755 100644 --- a/Session/Onboarding/LandingVC.swift +++ b/Session/Onboarding/LandingVC.swift @@ -65,7 +65,7 @@ final class LandingVC: BaseVC { linkButtonContainer.set(.height, to: Values.onboardingButtonBottomOffset) linkButtonContainer.addSubview(linkButton) linkButton.center(.horizontal, in: linkButtonContainer) - let isIPhoneX = (UIApplication.shared.keyWindow!.safeAreaInsets.bottom > 0) + let isIPhoneX = ((UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0) > 0) linkButton.centerYAnchor.constraint(equalTo: linkButtonContainer.centerYAnchor, constant: isIPhoneX ? -4 : 0).isActive = true // Button stack view let buttonStackView = UIStackView(arrangedSubviews: [ registerButton, restoreButton ]) diff --git a/Session/Shared/FullConversationCell.swift b/Session/Shared/FullConversationCell.swift index 14faf6618..8739de5c5 100644 --- a/Session/Shared/FullConversationCell.swift +++ b/Session/Shared/FullConversationCell.swift @@ -164,12 +164,14 @@ public final class FullConversationCell: UITableViewCell { // Unread count view unreadCountView.addSubview(unreadCountLabel) + unreadCountLabel.setCompressionResistanceHigh() unreadCountLabel.pin([ VerticalEdge.top, VerticalEdge.bottom ], to: unreadCountView) unreadCountView.pin(.leading, to: .leading, of: unreadCountLabel, withInset: -4) unreadCountView.pin(.trailing, to: .trailing, of: unreadCountLabel, withInset: 4) // Has mention view hasMentionView.addSubview(hasMentionLabel) + hasMentionLabel.setCompressionResistanceHigh() hasMentionLabel.pin(to: hasMentionView) // Label stack view diff --git a/Session/Shared/UserCell.swift b/Session/Shared/UserCell.swift index 546899e50..45a523552 100644 --- a/Session/Shared/UserCell.swift +++ b/Session/Shared/UserCell.swift @@ -11,6 +11,7 @@ final class UserCell: UITableViewCell { case none case lock case tick(isSelected: Bool) + case x } // MARK: - Components @@ -101,13 +102,14 @@ final class UserCell: UITableViewCell { to: contentView ) } - + // MARK: - Updating func update( with publicKey: String, profile: Profile?, isZombie: Bool, + mediumFont: Bool = false, accessory: Accessory ) { profilePictureView.update( @@ -116,11 +118,19 @@ final class UserCell: UITableViewCell { threadVariant: .contact ) - displayNameLabel.text = Profile.displayName( - for: .contact, - id: publicKey, - name: profile?.name, - nickname: profile?.nickname + displayNameLabel.font = (mediumFont ? + .systemFont(ofSize: Values.mediumFontSize) : + .boldSystemFont(ofSize: Values.mediumFontSize) + ) + + displayNameLabel.text = (getUserHexEncodedPublicKey() == publicKey ? + "MEDIA_GALLERY_SENDER_NAME_YOU".localized() : + Profile.displayName( + for: .contact, + id: publicKey, + name: profile?.name, + nickname: profile?.nickname + ) ) switch accessory { @@ -136,6 +146,11 @@ final class UserCell: UITableViewCell { accessoryImageView.isHidden = false accessoryImageView.image = icon.withRenderingMode(.alwaysTemplate) accessoryImageView.tintColor = Colors.text + case .x: + accessoryImageView.isHidden = false + accessoryImageView.image = #imageLiteral(resourceName: "X").withRenderingMode(.alwaysTemplate) + accessoryImageView.contentMode = .center + accessoryImageView.tintColor = Colors.text } let alpha: CGFloat = (isZombie ? 0.5 : 1) diff --git a/Session/Utilities/BackgroundPoller.swift b/Session/Utilities/BackgroundPoller.swift index 3ae9d0a03..faef01712 100644 --- a/Session/Utilities/BackgroundPoller.swift +++ b/Session/Utilities/BackgroundPoller.swift @@ -34,7 +34,7 @@ public final class BackgroundPoller { poller.stop() return poller.poll( - isBackgroundPoll: true, + calledFromBackgroundPoller: true, isBackgroundPollerValid: { BackgroundPoller.isValid }, isPostCapabilitiesRetry: false ) @@ -82,7 +82,7 @@ public final class BackgroundPoller { groupPublicKey, on: DispatchQueue.main, maxRetryCount: 0, - isBackgroundPoll: true, + calledFromBackgroundPoller: true, isBackgroundPollValid: { BackgroundPoller.isValid } ) } @@ -134,7 +134,7 @@ public final class BackgroundPoller { threadId: threadId, details: MessageReceiveJob.Details( messages: threadMessages.map { $0.messageInfo }, - isBackgroundPoll: true + calledFromBackgroundPoller: true ) ) diff --git a/Session/Utilities/DifferenceKit+Utilities.swift b/Session/Utilities/DifferenceKit+Utilities.swift index 23f6517d3..99c37931f 100644 --- a/Session/Utilities/DifferenceKit+Utilities.swift +++ b/Session/Utilities/DifferenceKit+Utilities.swift @@ -2,6 +2,7 @@ import Foundation import DifferenceKit +import SignalUtilitiesKit public extension ArraySection { init(section: Model, elements: [Element] = []) { diff --git a/Session/Utilities/IP2Country.swift b/Session/Utilities/IP2Country.swift index 052331a3d..a1f13ebab 100644 --- a/Session/Utilities/IP2Country.swift +++ b/Session/Utilities/IP2Country.swift @@ -50,8 +50,8 @@ final class IP2Country { @objc func populateCacheIfNeededAsync() { // This has to be sync since the `countryNamesCache` dict doesn't like async access - IP2Country.workQueue.sync { - let _ = self.populateCacheIfNeeded() + IP2Country.workQueue.sync { [weak self] in + _ = self?.populateCacheIfNeeded() } } diff --git a/SessionMessagingKit/Common Networking/QueryParam.swift b/SessionMessagingKit/Common Networking/QueryParam.swift index 7a1fe0f18..d50ffbab5 100644 --- a/SessionMessagingKit/Common Networking/QueryParam.swift +++ b/SessionMessagingKit/Common Networking/QueryParam.swift @@ -9,4 +9,7 @@ enum QueryParam: String { case required = "required" case limit // For messages - number between 1 and 256 (default is 100) case platform // For file server session version check + case updateTypes = "t" // String indicating the types of updates that the client supports + + case reactors = "reactors" } diff --git a/SessionMessagingKit/Common Networking/UpdateTypes.swift b/SessionMessagingKit/Common Networking/UpdateTypes.swift new file mode 100644 index 000000000..b245f53d1 --- /dev/null +++ b/SessionMessagingKit/Common Networking/UpdateTypes.swift @@ -0,0 +1,7 @@ +// Copyright Š 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +enum UpdateTypes: String { + case reaction = "r" +} diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 2230a04de..54e1b0258 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -18,7 +18,11 @@ public enum SNMessagingKit { // Just to make the external API nice ], [ _005_FixDeletedMessageReadState.self, - _006_FixHiddenModAdminSupport.self + _006_FixHiddenModAdminSupport.self, + _007_HomeQueryOptimisationIndexes.self + ], + [ + _008_EmojiReacts.self ] ] ) diff --git a/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift index 61747d7ea..385eb2260 100644 --- a/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift @@ -175,7 +175,7 @@ enum _001_InitialSetupMigration: Migration { .notNull() } - try db.create(table: GroupMember.self) { t in + try db.create(table: _006_FixHiddenModAdminSupport.PreMigrationGroupMember.self) { t in // Note: Since we don't know whether this will be stored against a 'ClosedGroup' or // an 'OpenGroup' we add the foreign key constraint against the thread itself (which // shares the same 'id' as the 'groupId') so we can cascade delete automatically diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index b748da16d..e5e565569 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -647,11 +647,10 @@ enum _003_YDBToGRDBMigration: Migration { } try groupModel.groupMemberIds.forEach { memberId in - try GroupMember( + try _006_FixHiddenModAdminSupport.PreMigrationGroupMember( groupId: threadId, profileId: memberId, - role: .standard, - isHidden: false + role: .standard ).insert(db) if !validProfileIds.contains(memberId) { @@ -660,11 +659,10 @@ enum _003_YDBToGRDBMigration: Migration { } try groupModel.groupAdminIds.forEach { adminId in - try GroupMember( + try _006_FixHiddenModAdminSupport.PreMigrationGroupMember( groupId: threadId, profileId: adminId, - role: .admin, - isHidden: false + role: .admin ).insert(db) if !validProfileIds.contains(adminId) { @@ -673,11 +671,10 @@ enum _003_YDBToGRDBMigration: Migration { } try (closedGroupZombieMemberIds[legacyThread.uniqueId] ?? []).forEach { zombieId in - try GroupMember( + try _006_FixHiddenModAdminSupport.PreMigrationGroupMember( groupId: threadId, profileId: zombieId, - role: .zombie, - isHidden: false + role: .zombie ).insert(db) if !validProfileIds.contains(zombieId) { @@ -1253,7 +1250,7 @@ enum _003_YDBToGRDBMigration: Migration { threadId: processedMessage.threadId, details: MessageReceiveJob.Details( messages: [processedMessage.messageInfo], - isBackgroundPoll: legacyJob.isBackgroundPoll + calledFromBackgroundPoller: legacyJob.isBackgroundPoll ) )?.inserted(db) } diff --git a/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift b/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift index c1097eb94..132266f45 100644 --- a/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift +++ b/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift @@ -28,3 +28,41 @@ enum _006_FixHiddenModAdminSupport: Migration { Storage.update(progress: 1, for: self, in: target) // In case this is the last migration } } + +// MARK: - Pre-Migration Types + +extension _006_FixHiddenModAdminSupport { + internal struct PreMigrationGroupMember: Codable, PersistableRecord, TableRecord, ColumnExpressible { + public static var databaseTableName: String { "groupMember" } + + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression { + case groupId + case profileId + case role + } + + public enum Role: Int, Codable, DatabaseValueConvertible { + case standard + case zombie + case moderator + case admin + } + + public let groupId: String + public let profileId: String + public let role: Role + + // MARK: - Initialization + + public init( + groupId: String, + profileId: String, + role: Role + ) { + self.groupId = groupId + self.profileId = profileId + self.role = role + } + } +} diff --git a/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift b/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift new file mode 100644 index 000000000..b468098f7 --- /dev/null +++ b/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift @@ -0,0 +1,37 @@ +// Copyright Š 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +/// This migration adds an index to the interaction table in order to improve the performance of retrieving the number of unread interactions +enum _007_HomeQueryOptimisationIndexes: Migration { + static let target: TargetMigrations.Identifier = .messagingKit + static let identifier: String = "HomeQueryOptimisationIndexes" + static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.1 + + static func migrate(_ db: Database) throws { + try db.create( + index: "interaction_on_wasRead_and_hasMention_and_threadId", + on: Interaction.databaseTableName, + columns: [ + Interaction.Columns.wasRead.name, + Interaction.Columns.hasMention.name, + Interaction.Columns.threadId.name + ] + ) + + try db.create( + index: "interaction_on_threadId_and_timestampMs_and_variant", + on: Interaction.databaseTableName, + columns: [ + Interaction.Columns.threadId.name, + Interaction.Columns.timestampMs.name, + Interaction.Columns.variant.name + ] + ) + + Storage.update(progress: 1, for: self, in: target) // In case this is the last migration + } +} diff --git a/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift b/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift new file mode 100644 index 000000000..b06687dca --- /dev/null +++ b/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift @@ -0,0 +1,42 @@ +// Copyright Š 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +/// This migration adds the new types needed for Emoji Reacts +enum _008_EmojiReacts: Migration { + static let target: TargetMigrations.Identifier = .messagingKit + static let identifier: String = "EmojiReacts" + static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.1 + + static func migrate(_ db: Database) throws { + try db.create(table: Reaction.self) { t in + t.column(.interactionId, .numeric) + .notNull() + .indexed() // Quicker querying + .references(Interaction.self, onDelete: .cascade) // Delete if Interaction deleted + t.column(.serverHash, .text) + t.column(.timestampMs, .text) + .notNull() + t.column(.authorId, .text) + .notNull() + .indexed() // Quicker querying + t.column(.emoji, .text) + .notNull() + .indexed() // Quicker querying + t.column(.count, .integer) + .notNull() + .defaults(to: 0) + t.column(.sortId, .integer) + .notNull() + .defaults(to: 0) + + /// A specific author should only be able to have a single instance of each emoji on a particular interaction + t.uniqueKey([.interactionId, .emoji, .authorId]) + } + + Storage.update(progress: 1, for: self, in: target) // In case this is the last migration + } +} diff --git a/SessionMessagingKit/Database/Models/Capability.swift b/SessionMessagingKit/Database/Models/Capability.swift index 9feda3eb1..304bc8813 100644 --- a/SessionMessagingKit/Database/Models/Capability.swift +++ b/SessionMessagingKit/Database/Models/Capability.swift @@ -16,11 +16,12 @@ public struct Capability: Codable, FetchableRecord, PersistableRecord, TableReco public enum Variant: Equatable, Hashable, CaseIterable, Codable, DatabaseValueConvertible { public static var allCases: [Variant] { - [.sogs, .blind] + [.sogs, .blind, .reactions] } case sogs case blind + case reactions /// Fallback case if the capability isn't supported by this version of the app case unsupported(String) diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index ccd696ad9..0b9c64e96 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -262,7 +262,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu self.body = body self.timestampMs = timestampMs self.receivedAtTimestampMs = receivedAtTimestampMs - self.wasRead = (wasRead && variant.canBeUnread) + self.wasRead = (wasRead || !variant.canBeUnread) self.hasMention = hasMention self.expiresInSeconds = expiresInSeconds self.expiresStartedAtMs = expiresStartedAtMs @@ -304,7 +304,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu default: return timestampMs } }() - self.wasRead = (wasRead && variant.canBeUnread) + self.wasRead = (wasRead || !variant.canBeUnread) self.hasMention = hasMention self.expiresInSeconds = expiresInSeconds self.expiresStartedAtMs = expiresStartedAtMs @@ -348,10 +348,14 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu ).insert(db) case .closedGroup: - guard - let closedGroup: ClosedGroup = try? thread.closedGroup.fetchOne(db), - let members: [GroupMember] = try? closedGroup.members.fetchAll(db) - else { + let closedGroupMemberIds: Set = (try? GroupMember + .select(.profileId) + .filter(GroupMember.Columns.groupId == thread.id) + .asRequest(of: String.self) + .fetchSet(db)) + .defaulting(to: []) + + guard !closedGroupMemberIds.isEmpty else { SNLog("Inserted an interaction but couldn't find it's associated thread members") return } @@ -359,12 +363,12 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu // Exclude the current user when creating recipient states (as they will never // receive the message resulting in the message getting flagged as failed) let userPublicKey: String = getUserHexEncodedPublicKey(db) - try members - .filter { member -> Bool in member.profileId != userPublicKey } - .forEach { member in + try closedGroupMemberIds + .filter { memberId -> Bool in memberId != userPublicKey } + .forEach { memberId in try RecipientState( interactionId: interactionId, - recipientId: member.profileId, + recipientId: memberId, state: .sending ).insert(db) } @@ -409,7 +413,7 @@ public extension Interaction { body: (body ?? self.body), timestampMs: (timestampMs ?? self.timestampMs), receivedAtTimestampMs: self.receivedAtTimestampMs, - wasRead: (wasRead ?? self.wasRead), + wasRead: ((wasRead ?? self.wasRead) || !self.variant.canBeUnread), hasMention: (hasMention ?? self.hasMention), expiresInSeconds: (expiresInSeconds ?? self.expiresInSeconds), expiresStartedAtMs: (expiresStartedAtMs ?? self.expiresStartedAtMs), @@ -453,6 +457,23 @@ public extension Interaction { ) ) + // Clear out any notifications for the interactions we mark as read + Environment.shared?.notificationsManager.wrappedValue?.cancelNotifications( + identifiers: interactionIds + .map { interactionId in + Interaction.notificationIdentifier( + for: interactionId, + threadId: threadId, + shouldGroupMessagesForThread: false + ) + } + .appending(Interaction.notificationIdentifier( + for: 0, + threadId: threadId, + shouldGroupMessagesForThread: true + )) + ) + // If we want to send read receipts then try to add the 'SendReadReceiptsJob' if trySendReadReceipt { JobRunner.upsert( @@ -573,18 +594,27 @@ public extension Interaction { var notificationIdentifiers: [String] { [ - notificationIdentifier(isBackgroundPoll: true), - notificationIdentifier(isBackgroundPoll: false) + notificationIdentifier(shouldGroupMessagesForThread: true), + notificationIdentifier(shouldGroupMessagesForThread: false) ] } // MARK: - Functions - func notificationIdentifier(isBackgroundPoll: Bool) -> String { + func notificationIdentifier(shouldGroupMessagesForThread: Bool) -> String { // When the app is in the background we want the notifications to be grouped to prevent spam - guard !isBackgroundPoll else { return threadId } + return Interaction.notificationIdentifier( + for: (id ?? 0), + threadId: threadId, + shouldGroupMessagesForThread: shouldGroupMessagesForThread + ) + } + + fileprivate static func notificationIdentifier(for id: Int64, threadId: String, shouldGroupMessagesForThread: Bool) -> String { + // When the app is in the background we want the notifications to be grouped to prevent spam + guard !shouldGroupMessagesForThread else { return threadId } - return "\(threadId)-\(id ?? 0)" + return "\(threadId)-\(id)" } func markingAsDeleted() -> Interaction { @@ -598,7 +628,7 @@ public extension Interaction { body: nil, timestampMs: timestampMs, receivedAtTimestampMs: receivedAtTimestampMs, - wasRead: (wasRead && Variant.standardIncomingDeleted.canBeUnread), + wasRead: (wasRead || !Variant.standardIncomingDeleted.canBeUnread), hasMention: hasMention, expiresInSeconds: expiresInSeconds, expiresStartedAtMs: expiresStartedAtMs, diff --git a/SessionMessagingKit/Database/Models/Reaction.swift b/SessionMessagingKit/Database/Models/Reaction.swift new file mode 100644 index 000000000..df10caad4 --- /dev/null +++ b/SessionMessagingKit/Database/Models/Reaction.swift @@ -0,0 +1,134 @@ +// Copyright Š 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +public struct Reaction: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { + public static var databaseTableName: String { "reaction" } + internal static let profileForeignKey = ForeignKey([Columns.authorId], to: [Profile.Columns.id]) + internal static let interactionForeignKey = ForeignKey([Columns.interactionId], to: [Interaction.Columns.id]) + private static let profile = hasOne(Profile.self, using: profileForeignKey) + internal static let interaction = belongsTo(Interaction.self, using: interactionForeignKey) + + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression { + case interactionId + case serverHash + case timestampMs + case authorId + case emoji + case count + case sortId + } + + /// The id for the interaction this reaction belongs to + public let interactionId: Int64 + + /// The server hash for this reaction in the swarm + /// + /// **Note:** This value will be `null` for reactions in open groups + public let serverHash: String? + + /// When the reaction was created in milliseconds since epoch + public let timestampMs: Int64 + + /// The id for the user who made this reaction + public let authorId: String + + /// The emoji for this reaction + public let emoji: String + + /// The number of times this emoji was used + /// + /// **Note:** This value will always be `1` for 1-1 messages and closed groups, but will be either `0` or + /// the total number of emoji's used in open groups (this allows us to `SUM` this column to get the official total + /// regardless of the type of conversation) + public let count: Int64 + + /// The first timestamp that an emoji is added + public let sortId: Int64 + + // MARK: - Relationships + + public var interaction: QueryInterfaceRequest { + request(for: Reaction.interaction) + } + + public var profile: QueryInterfaceRequest { + request(for: Reaction.profile) + } + + // MARK: - Initialization + + public init( + interactionId: Int64, + serverHash: String?, + timestampMs: Int64, + authorId: String, + emoji: String, + count: Int64, + sortId: Int64 + ) { + self.interactionId = interactionId + self.serverHash = serverHash + self.timestampMs = timestampMs + self.authorId = authorId + self.emoji = emoji + self.count = count + self.sortId = sortId + } +} + +// MARK: - Mutation + +public extension Reaction { + func with( + interactionId: Int64? = nil, + serverHash: String? = nil, + authorId: String? = nil, + count: Int64? = nil, + sortId: Int64? = nil + ) -> Reaction { + return Reaction( + interactionId: (interactionId ?? self.interactionId), + serverHash: (serverHash ?? self.serverHash), + timestampMs: self.timestampMs, + authorId: (authorId ?? self.authorId), + emoji: self.emoji, + count: (count ?? self.count), + sortId: (sortId ?? self.sortId) + ) + } +} + +// MARK: - SortId + +public extension Reaction { + static func getSortId( + _ db: Database, + interactionId: Int64, + emoji: String + ) -> Int64 { + if let existingSortId: Int64 = try? Reaction + .select(Columns.sortId) + .filter(Columns.interactionId == interactionId) + .filter(Columns.emoji == emoji) + .asRequest(of: Int64.self) + .fetchOne(db) + { + return existingSortId + } + + if let existingLargestSortId: Int64 = try? Reaction + .select(max(Columns.sortId)) + .filter(Columns.interactionId == interactionId) + .asRequest(of: Int64.self) + .fetchOne(db) + { + return existingLargestSortId + 1 + } + + return 0 + } +} diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 452ab8c49..86c8c8629 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -202,23 +202,24 @@ public extension SessionThread { """ } - static func unreadMessageRequestsThreadIdQuery(userPublicKey: String, includeNonVisible: Bool = false) -> SQLRequest { + static func unreadMessageRequestsCountQuery(userPublicKey: String, includeNonVisible: Bool = false) -> SQLRequest { let thread: TypedTableAlias = TypedTableAlias() let interaction: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() return """ - SELECT \(thread[.id]) - FROM \(SessionThread.self) - JOIN \(Interaction.self) ON ( - \(interaction[.threadId]) = \(thread[.id]) AND - \(interaction[.wasRead]) = false + SELECT COUNT(DISTINCT id) FROM ( + SELECT \(thread[.id]) AS id + FROM \(SessionThread.self) + JOIN \(Interaction.self) ON ( + \(interaction[.threadId]) = \(thread[.id]) AND + \(interaction[.wasRead]) = false + ) + LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) + WHERE ( + \(SessionThread.isMessageRequest(userPublicKey: userPublicKey, includeNonVisible: includeNonVisible)) + ) ) - LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) - WHERE ( - \(SessionThread.isMessageRequest(userPublicKey: userPublicKey, includeNonVisible: includeNonVisible)) - ) - GROUP BY \(thread[.id]) """ } @@ -276,8 +277,8 @@ public extension SessionThread { // all the other message request threads have been read if !hasHiddenMessageRequests { let numUnreadMessageRequestThreads: Int = (try? SessionThread - .unreadMessageRequestsThreadIdQuery(userPublicKey: userPublicKey, includeNonVisible: true) - .fetchCount(db)) + .unreadMessageRequestsCountQuery(userPublicKey: userPublicKey, includeNonVisible: true) + .fetchOne(db)) .defaulting(to: 1) guard numUnreadMessageRequestThreads == 1 else { return false } diff --git a/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift b/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift index 6822f1fe0..582c5aae1 100644 --- a/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift +++ b/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift @@ -37,8 +37,7 @@ public enum MessageReceiveJob: JobExecutor { db, message: messageInfo.message, associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData), - openGroupId: nil, - isBackgroundPoll: details.isBackgroundPoll + openGroupId: nil ) } catch { @@ -76,7 +75,7 @@ public enum MessageReceiveJob: JobExecutor { .with( details: Details( messages: remainingMessagesToProcess, - isBackgroundPoll: details.isBackgroundPoll + calledFromBackgroundPoller: details.calledFromBackgroundPoller ) ) .defaulting(to: job) @@ -164,14 +163,18 @@ extension MessageReceiveJob { } public let messages: [MessageInfo] - public let isBackgroundPoll: Bool + private let isBackgroundPoll: Bool + + // Renamed variable for clarity (and didn't want to migrate old MessageReceiveJob + // values so didn't rename the original) + public var calledFromBackgroundPoller: Bool { isBackgroundPoll } public init( messages: [MessageInfo], - isBackgroundPoll: Bool + calledFromBackgroundPoller: Bool ) { self.messages = messages - self.isBackgroundPoll = isBackgroundPoll + self.isBackgroundPoll = calledFromBackgroundPoller } } } diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index e33da9647..dbd6acc86 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -3,6 +3,7 @@ import Foundation import GRDB import SessionSnodeKit +import SessionUtilitiesKit /// Abstract base class for `VisibleMessage` and `ControlMessage`. public class Message: Codable { @@ -291,10 +292,10 @@ public extension Message { dependencies: SMKDependencies = SMKDependencies() ) throws -> ProcessedMessage? { // Need a sender in order to process the message - guard let sender: String = message.sender else { return nil } + guard let sender: String = message.sender, let timestamp = message.posted else { return nil } // Note: The `posted` value is in seconds but all messages in the database use milliseconds for timestamps - let envelopeBuilder = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: UInt64(floor(message.posted * 1000))) + let envelopeBuilder = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: UInt64(floor(timestamp * 1000))) envelopeBuilder.setContent(data) envelopeBuilder.setSource(sender) @@ -348,6 +349,123 @@ public extension Message { ) } + static func processRawReceivedReactions( + _ db: Database, + openGroupId: String, + message: OpenGroupAPI.Message, + associatedPendingChanges: [OpenGroupAPI.PendingChange], + dependencies: SMKDependencies = SMKDependencies() + ) -> [Reaction] { + var results: [Reaction] = [] + guard let reactions = message.reactions else { return results } + let userPublicKey: String = getUserHexEncodedPublicKey(db) + let blindedUserPublicKey: String? = SessionThread + .getUserHexEncodedBlindedKey( + threadId: openGroupId, + threadVariant: .openGroup + ) + for (encodedEmoji, rawReaction) in reactions { + if let decodedEmoji = encodedEmoji.removingPercentEncoding, + rawReaction.count > 0, + let reactors = rawReaction.reactors + { + // Decide whether we need to ignore all reactions + let pendingChangeRemoveAllReaction: Bool = associatedPendingChanges.contains { pendingChange in + if case .reaction(_, let emoji, let action) = pendingChange.metadata { + return emoji == decodedEmoji && action == .removeAll + } + return false + } + + // Decide whether we need to add an extra reaction from current user + let pendingChangeSelfReaction: Bool? = { + // Find the newest 'PendingChange' entry with a matching emoji, if one exists, and + // set the "self reaction" value based on it's action + let maybePendingChange: OpenGroupAPI.PendingChange? = associatedPendingChanges + .sorted(by: { lhs, rhs -> Bool in (lhs.seqNo ?? Int64.max) >= (rhs.seqNo ?? Int64.max) }) + .first { pendingChange in + if case .reaction(_, let emoji, _) = pendingChange.metadata { + return emoji == decodedEmoji + } + + return false + } + + // If there is no pending change for this reaction then return nil + guard + let pendingChange: OpenGroupAPI.PendingChange = maybePendingChange, + case .reaction(_, _, let action) = pendingChange.metadata + else { return nil } + + // Otherwise add/remove accordingly + return action == .add + }() + let shouldAddSelfReaction: Bool = ( + pendingChangeSelfReaction ?? + ((rawReaction.you || reactors.contains(userPublicKey)) && !pendingChangeRemoveAllReaction) + ) + + let count: Int64 = rawReaction.you ? rawReaction.count - 1 : rawReaction.count + + let timestampMs: Int64 = Int64(floor((Date().timeIntervalSince1970 * 1000))) + let maxLength: Int = shouldAddSelfReaction ? 4 : 5 + let desiredReactorIds: [String] = reactors + .filter { $0 != blindedUserPublicKey && $0 != userPublicKey } // Remove current user for now, will add back if needed + .prefix(maxLength) + .map{ $0 } + + results = results + .appending( // Add the first reaction (with the count) + pendingChangeRemoveAllReaction ? + nil : + desiredReactorIds.first + .map { reactor in + Reaction( + interactionId: message.id, + serverHash: nil, + timestampMs: timestampMs, + authorId: reactor, + emoji: decodedEmoji, + count: count, + sortId: rawReaction.index + ) + } + ) + .appending( // Add all other reactions + contentsOf: desiredReactorIds.count <= 1 || pendingChangeRemoveAllReaction ? + [] : + desiredReactorIds + .suffix(from: 1) + .map { reactor in + Reaction( + interactionId: message.id, + serverHash: nil, + timestampMs: timestampMs, + authorId: reactor, + emoji: decodedEmoji, + count: 0, // Only want this on the first reaction + sortId: rawReaction.index + ) + } + ) + .appending( // Add the current user reaction (if applicable and not already included) + !shouldAddSelfReaction ? + nil : + Reaction( + interactionId: message.id, + serverHash: nil, + timestampMs: timestampMs, + authorId: userPublicKey, + emoji: decodedEmoji, + count: 1, + sortId: rawReaction.index + ) + ) + } + } + return results + } + private static func processRawReceivedMessage( _ db: Database, envelope: SNProtoEnvelope, diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Quote.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Quote.swift index fdd50732c..6adeb6088 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Quote.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Quote.swift @@ -35,7 +35,7 @@ public extension VisibleMessage { } public func toProto() -> SNProtoDataMessageQuote? { - preconditionFailure("Use toProto(using:) instead.") + preconditionFailure("Use toProto(_:) instead.") } public func toProto(_ db: Database) -> SNProtoDataMessageQuote? { diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Reaction.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Reaction.swift new file mode 100644 index 000000000..21c53ba9e --- /dev/null +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Reaction.swift @@ -0,0 +1,106 @@ +// Copyright Š 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +public extension VisibleMessage { + struct VMReaction: Codable { + /// This is the timestamp (in milliseconds since epoch) when the interaction this reaction belongs to was sent + public var timestamp: UInt64 + + /// This is the public key of the sender of the interaction this reaction belongs to + public var publicKey: String + + /// This is the emoji for the reaction + public var emoji: String + + /// This is the behaviour for the reaction + public var kind: Kind + + public var isValid: Bool { true } + + // MARK: - Kind + + public enum Kind: Int, Codable { + case react + case remove + + var description: String { + switch self { + case .react: return "react" + case .remove: return "remove" + } + } + + // MARK: - Initialization + + init(protoAction: SNProtoDataMessageReaction.SNProtoDataMessageReactionAction) { + switch protoAction { + case .react: self = .react + case .remove: self = .remove + } + } + + // MARK: - Proto Conversion + + func toProto() -> SNProtoDataMessageReaction.SNProtoDataMessageReactionAction { + switch self { + case .react: return .react + case .remove: return .remove + } + } + } + + // MARK: - Initialization + + public init(timestamp: UInt64, publicKey: String, emoji: String, kind: Kind) { + self.timestamp = timestamp + self.publicKey = publicKey + self.emoji = emoji + self.kind = kind + } + + // MARK: - Proto Conversion + + public static func fromProto(_ proto: SNProtoDataMessageReaction) -> VMReaction? { + guard let emoji: String = proto.emoji else { return nil } + + return VMReaction( + timestamp: proto.id, + publicKey: proto.author, + emoji: emoji, + kind: Kind(protoAction: proto.action) + ) + } + + public func toProto() -> SNProtoDataMessageReaction? { + let reactionProto = SNProtoDataMessageReaction.builder( + id: self.timestamp, + author: self.publicKey, + action: self.kind.toProto() + ) + reactionProto.setEmoji(self.emoji) + + do { + return try reactionProto.build() + } catch { + SNLog("Couldn't construct quote proto from: \(self).") + return nil + } + } + + // MARK: - Description + + public var description: String { + """ + Reaction( + timestamp: \(timestamp), + publicKey: \(publicKey), + emoji: \(emoji), + kind: \(kind.description) + ) + """ + } + } +} diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift index 698baa732..0e043ef87 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift @@ -13,6 +13,7 @@ public final class VisibleMessage: Message { case linkPreview case profile case openGroupInvitation + case reaction } /// In the case of a sync message, the public key of the person the message was targeted at. @@ -25,6 +26,7 @@ public final class VisibleMessage: Message { public let linkPreview: VMLinkPreview? public var profile: VMProfile? public let openGroupInvitation: VMOpenGroupInvitation? + public let reaction: VMReaction? public override var isSelfSendValid: Bool { true } @@ -34,6 +36,7 @@ public final class VisibleMessage: Message { guard super.isValid else { return false } if !attachmentIds.isEmpty { return true } if openGroupInvitation != nil { return true } + if reaction != nil { return true } if let text = text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty { return true } return false } @@ -50,7 +53,8 @@ public final class VisibleMessage: Message { quote: VMQuote? = nil, linkPreview: VMLinkPreview? = nil, profile: VMProfile? = nil, - openGroupInvitation: VMOpenGroupInvitation? = nil + openGroupInvitation: VMOpenGroupInvitation? = nil, + reaction: VMReaction? = nil ) { self.syncTarget = syncTarget self.text = text @@ -59,6 +63,7 @@ public final class VisibleMessage: Message { self.linkPreview = linkPreview self.profile = profile self.openGroupInvitation = openGroupInvitation + self.reaction = reaction super.init( sentTimestamp: sentTimestamp, @@ -79,6 +84,7 @@ public final class VisibleMessage: Message { linkPreview = try? container.decode(VMLinkPreview.self, forKey: .linkPreview) profile = try? container.decode(VMProfile.self, forKey: .profile) openGroupInvitation = try? container.decode(VMOpenGroupInvitation.self, forKey: .openGroupInvitation) + reaction = try? container.decode(VMReaction.self, forKey: .reaction) try super.init(from: decoder) } @@ -95,6 +101,7 @@ public final class VisibleMessage: Message { try container.encodeIfPresent(linkPreview, forKey: .linkPreview) try container.encodeIfPresent(profile, forKey: .profile) try container.encodeIfPresent(openGroupInvitation, forKey: .openGroupInvitation) + try container.encodeIfPresent(reaction, forKey: .reaction) } // MARK: - Proto Conversion @@ -109,7 +116,8 @@ public final class VisibleMessage: Message { quote: dataMessage.quote.map { VMQuote.fromProto($0) }, linkPreview: dataMessage.preview.first.map { VMLinkPreview.fromProto($0) }, profile: VMProfile.fromProto(dataMessage), - openGroupInvitation: dataMessage.openGroupInvitation.map { VMOpenGroupInvitation.fromProto($0) } + openGroupInvitation: dataMessage.openGroupInvitation.map { VMOpenGroupInvitation.fromProto($0) }, + reaction: dataMessage.reaction.map { VMReaction.fromProto($0) } ) } @@ -168,6 +176,11 @@ public final class VisibleMessage: Message { dataMessage.setOpenGroupInvitation(openGroupInvitationProto) } + // Emoji react + if let reaction = reaction, let reactionProto = reaction.toProto() { + dataMessage.setReaction(reactionProto) + } + // Group context do { try setGroupContextIfNeeded(db, on: dataMessage) @@ -175,10 +188,12 @@ public final class VisibleMessage: Message { SNLog("Couldn't construct visible message proto from: \(self).") return nil } + // Sync target if let syncTarget = syncTarget { dataMessage.setSyncTarget(syncTarget) } + // Build do { proto.setDataMessage(try dataMessage.build()) @@ -198,8 +213,9 @@ public final class VisibleMessage: Message { attachmentIds: \(attachmentIds), quote: \(quote?.description ?? "null"), linkPreview: \(linkPreview?.description ?? "null"), - profile: \(profile?.description ?? "null") - "openGroupInvitation": \(openGroupInvitation?.description ?? "null") + profile: \(profile?.description ?? "null"), + reaction: \(reaction?.description ?? "null"), + openGroupInvitation: \(openGroupInvitation?.description ?? "null") ) """ } @@ -239,7 +255,8 @@ public extension VisibleMessage { db, linkPreview: linkPreview ) - } + }, + reaction: nil // Reactions are custom messages sent separately ) } } diff --git a/SessionMessagingKit/Meta/SessionMessagingKit.h b/SessionMessagingKit/Meta/SessionMessagingKit.h index a0fda3d4a..1a9c8d33f 100644 --- a/SessionMessagingKit/Meta/SessionMessagingKit.h +++ b/SessionMessagingKit/Meta/SessionMessagingKit.h @@ -6,5 +6,4 @@ FOUNDATION_EXPORT const unsigned char SessionMessagingKitVersionString[]; #import #import #import -#import #import diff --git a/SessionMessagingKit/Open Groups/Models/PendingChange.swift b/SessionMessagingKit/Open Groups/Models/PendingChange.swift new file mode 100644 index 000000000..dd5af98b5 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/PendingChange.swift @@ -0,0 +1,47 @@ +// Copyright Š 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPI { + public struct PendingChange: Equatable { + public enum ChangeType { + case reaction + } + + public enum ReactAction: Equatable { + case add + case remove + case removeAll + } + + enum Metadata { + case reaction(messageId: Int64, emoji: String, action: ReactAction) + } + + let server: String + let room: String + let changeType: ChangeType + var seqNo: Int64? + let metadata: Metadata + + public static func == (lhs: OpenGroupAPI.PendingChange, rhs: OpenGroupAPI.PendingChange) -> Bool { + guard lhs.server == rhs.server && + lhs.room == rhs.room && + lhs.changeType == rhs.changeType && + lhs.seqNo == rhs.seqNo + else { + return false + } + + switch lhs.changeType { + case .reaction: + if case .reaction(let lhsMessageId, let lhsEmoji, let lhsAction) = lhs.metadata, + case .reaction(let rhsMessageId, let rhsEmoji, let rhsAction) = rhs.metadata { + return lhsMessageId == rhsMessageId && lhsEmoji == rhsEmoji && lhsAction == rhsAction + } else { + return false + } + } + } + } +} diff --git a/SessionMessagingKit/Open Groups/Models/ReactionResponse.swift b/SessionMessagingKit/Open Groups/Models/ReactionResponse.swift new file mode 100644 index 000000000..cfded186d --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/ReactionResponse.swift @@ -0,0 +1,44 @@ +// Copyright Š 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPI { + public struct ReactionAddResponse: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case added + case seqNo = "seqno" + } + + /// This field indicates whether the reaction was added (true) or already present (false). + public let added: Bool + + /// The seqNo after the reaction is added. + public let seqNo: Int64? + } + + public struct ReactionRemoveResponse: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case removed + case seqNo = "seqno" + } + + /// This field indicates whether the reaction was removed (true) or was not present to begin with (false). + public let removed: Bool + + /// The seqNo after the reaction is removed. + public let seqNo: Int64? + } + + public struct ReactionRemoveAllResponse: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case removed + case seqNo = "seqno" + } + + /// This field shows the total number of reactions that were deleted. + public let removed: Int64 + + /// The seqNo after the reactions is all removed. + public let seqNo: Int64? + } +} diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index f566565f7..8ce774b31 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -18,11 +18,13 @@ extension OpenGroupAPI { case base64EncodedData = "data" case base64EncodedSignature = "signature" + + case reactions = "reactions" } public let id: Int64 public let sender: String? - public let posted: TimeInterval + public let posted: TimeInterval? public let edited: TimeInterval? public let deleted: Bool? public let seqNo: Int64 @@ -32,6 +34,22 @@ extension OpenGroupAPI { public let base64EncodedData: String? public let base64EncodedSignature: String? + + public struct Reaction: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case count + case reactors + case you + case index + } + + public let count: Int64 + public let reactors: [String]? + public let you: Bool + public let index: Int64 + } + + public let reactions: [String:Reaction]? } } @@ -44,6 +62,7 @@ extension OpenGroupAPI.Message { let maybeSender: String? = try? container.decode(String.self, forKey: .sender) let maybeBase64EncodedData: String? = try? container.decode(String.self, forKey: .base64EncodedData) let maybeBase64EncodedSignature: String? = try? container.decode(String.self, forKey: .base64EncodedSignature) + let maybeReactions: [String:Reaction]? = try? container.decode([String:Reaction].self, forKey: .reactions) // If we have data and a signature (ie. the message isn't a deletion) then validate the signature if let base64EncodedData: String = maybeBase64EncodedData, let base64EncodedSignature: String = maybeBase64EncodedSignature { @@ -79,7 +98,7 @@ extension OpenGroupAPI.Message { self = OpenGroupAPI.Message( id: try container.decode(Int64.self, forKey: .id), sender: try? container.decode(String.self, forKey: .sender), - posted: try container.decode(TimeInterval.self, forKey: .posted), + posted: try? container.decode(TimeInterval.self, forKey: .posted), edited: try? container.decode(TimeInterval.self, forKey: .edited), deleted: try? container.decode(Bool.self, forKey: .deleted), seqNo: try container.decode(Int64.self, forKey: .seqNo), @@ -87,7 +106,21 @@ extension OpenGroupAPI.Message { whisperMods: ((try? container.decode(Bool.self, forKey: .whisperMods)) ?? false), whisperTo: try? container.decode(String.self, forKey: .whisperTo), base64EncodedData: maybeBase64EncodedData, - base64EncodedSignature: maybeBase64EncodedSignature + base64EncodedSignature: maybeBase64EncodedSignature, + reactions: !container.contains(.reactions) ? nil : (maybeReactions ?? [:]) + ) + } +} + +extension OpenGroupAPI.Message.Reaction { + public init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + self = OpenGroupAPI.Message.Reaction( + count: try container.decode(Int64.self, forKey: .count), + reactors: try? container.decode([String].self, forKey: .reactors), + you: (try? container.decode(Bool.self, forKey: .you)) ?? false, + index: (try container.decode(Int64.self, forKey: .index)) ) } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 60011301e..04cf63b57 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -96,7 +96,11 @@ public enum OpenGroupAPI { endpoint: (shouldRetrieveRecentMessages ? .roomMessagesRecent(openGroup.roomToken) : .roomMessagesSince(openGroup.roomToken, seqNo: openGroup.sequenceNumber) - ) + ), + queryParameters: [ + .updateTypes: UpdateTypes.reaction.rawValue, + .reactors: "5" + ] ), responseType: [Failable].self ) @@ -618,7 +622,11 @@ public enum OpenGroupAPI { db, request: Request( server: server, - endpoint: .roomMessagesSince(roomToken, seqNo: seqNo) + endpoint: .roomMessagesSince(roomToken, seqNo: seqNo), + queryParameters: [ + .updateTypes: UpdateTypes.reaction.rawValue, + .reactors: "20" + ] ), using: dependencies ) @@ -657,6 +665,116 @@ public enum OpenGroupAPI { ) } + // MARK: - Reactions + + public static func reactors( + _ db: Database, + emoji: String, + id: Int64, + in roomToken: String, + on server: String, + using dependencies: SMKDependencies = SMKDependencies() + ) -> Promise { + /// URL(String:) won't convert raw emojis, so need to do a little encoding here. + /// The raw emoji will come back when calling url.path + guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { + return Promise(error: OpenGroupAPIError.invalidEmoji) + } + + return OpenGroupAPI + .send( + db, + request: Request( + method: .get, + server: server, + endpoint: .reactors(roomToken, id: id, emoji: encodedEmoji) + ), + using: dependencies + ) + .map { responseInfo, _ in responseInfo } + } + + public static func reactionAdd( + _ db: Database, + emoji: String, + id: Int64, + in roomToken: String, + on server: String, + using dependencies: SMKDependencies = SMKDependencies() + ) -> Promise<(OnionRequestResponseInfoType, ReactionAddResponse)> { + /// URL(String:) won't convert raw emojis, so need to do a little encoding here. + /// The raw emoji will come back when calling url.path + guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { + return Promise(error: OpenGroupAPIError.invalidEmoji) + } + + return OpenGroupAPI + .send( + db, + request: Request( + method: .put, + server: server, + endpoint: .reaction(roomToken, id: id, emoji: encodedEmoji) + ), + using: dependencies + ) + .decoded(as: ReactionAddResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) + } + + public static func reactionDelete( + _ db: Database, + emoji: String, + id: Int64, + in roomToken: String, + on server: String, + using dependencies: SMKDependencies = SMKDependencies() + ) -> Promise<(OnionRequestResponseInfoType, ReactionRemoveResponse)> { + /// URL(String:) won't convert raw emojis, so need to do a little encoding here. + /// The raw emoji will come back when calling url.path + guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { + return Promise(error: OpenGroupAPIError.invalidEmoji) + } + + return OpenGroupAPI + .send( + db, + request: Request( + method: .delete, + server: server, + endpoint: .reaction(roomToken, id: id, emoji: encodedEmoji) + ), + using: dependencies + ) + .decoded(as: ReactionRemoveResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) + } + + public static func reactionDeleteAll( + _ db: Database, + emoji: String, + id: Int64, + in roomToken: String, + on server: String, + using dependencies: SMKDependencies = SMKDependencies() + ) -> Promise<(OnionRequestResponseInfoType, ReactionRemoveAllResponse)> { + /// URL(String:) won't convert raw emojis, so need to do a little encoding here. + /// The raw emoji will come back when calling url.path + guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { + return Promise(error: OpenGroupAPIError.invalidEmoji) + } + + return OpenGroupAPI + .send( + db, + request: Request( + method: .delete, + server: server, + endpoint: .reactionDelete(roomToken, id: id, emoji: encodedEmoji) + ), + using: dependencies + ) + .decoded(as: ReactionRemoveAllResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) + } + // MARK: - Pinning /// Adds a pinned message to this room diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 3e8370ddb..7afea37a0 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -19,6 +19,8 @@ public protocol OGMCacheType { var hasPerformedInitialPoll: [String: Bool] { get set } var timeSinceLastPoll: [String: TimeInterval] { get set } + var pendingChanges: [OpenGroupAPI.PendingChange] { get set } + func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval } @@ -53,6 +55,8 @@ public final class OpenGroupManager: NSObject { _timeSinceLastOpen = dependencies.date.timeIntervalSince(lastOpen) return dependencies.date.timeIntervalSince(lastOpen) } + + public var pendingChanges: [OpenGroupAPI.PendingChange] = [] } // MARK: - Variables @@ -512,7 +516,6 @@ public final class OpenGroupManager: NSObject { messages: [OpenGroupAPI.Message], for roomToken: String, on server: String, - isBackgroundPoll: Bool, dependencies: OGMDependencies = OGMDependencies() ) { // Sorting the messages by server ID before importing them fixes an issue where messages @@ -530,56 +533,95 @@ public final class OpenGroupManager: NSObject { .filter { $0.deleted == true } .map { $0.id } - // Update the 'openGroupSequenceNumber' value (Note: SOGS V4 uses the 'seqNo' instead of the 'serverId') if let seqNo: Int64 = seqNo { + // Update the 'openGroupSequenceNumber' value (Note: SOGS V4 uses the 'seqNo' instead of the 'serverId') _ = try? OpenGroup .filter(id: openGroup.id) .updateAll(db, OpenGroup.Columns.sequenceNumber.set(to: seqNo)) + + // Update pendingChange cache + dependencies.mutableCache.mutate { + $0.pendingChanges = $0.pendingChanges + .filter { $0.seqNo == nil || $0.seqNo! > seqNo } + } } // Process the messages sortedMessages.forEach { message in - guard - let base64EncodedString: String = message.base64EncodedData, - let data = Data(base64Encoded: base64EncodedString) - else { - // FIXME: Once the SOGS Emoji Reacts update is live we should remove this line (deprecated by the `deleted` flag) + if message.base64EncodedData == nil && message.reactions == nil { messageServerIdsToRemove.append(Int64(message.id)) return } - do { - let processedMessage: ProcessedMessage? = try Message.processReceivedOpenGroupMessage( - db, - openGroupId: openGroup.id, - openGroupServerPublicKey: openGroup.publicKey, - message: message, - data: data, - dependencies: dependencies - ) - - if let messageInfo: MessageReceiveJob.Details.MessageInfo = processedMessage?.messageInfo { - try MessageReceiver.handle( + // Handle messages + if let base64EncodedString: String = message.base64EncodedData, + let data = Data(base64Encoded: base64EncodedString) + { + do { + let processedMessage: ProcessedMessage? = try Message.processReceivedOpenGroupMessage( db, - message: messageInfo.message, - associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData), openGroupId: openGroup.id, - isBackgroundPoll: isBackgroundPoll, + openGroupServerPublicKey: openGroup.publicKey, + message: message, + data: data, dependencies: dependencies ) + + if let messageInfo: MessageReceiveJob.Details.MessageInfo = processedMessage?.messageInfo { + try MessageReceiver.handle( + db, + message: messageInfo.message, + associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData), + openGroupId: openGroup.id, + dependencies: dependencies + ) + } + } + catch { + switch error { + // Ignore duplicate & selfSend message errors (and don't bother logging + // them as there will be a lot since we each service node duplicates messages) + case DatabaseError.SQLITE_CONSTRAINT_UNIQUE, + MessageReceiverError.duplicateMessage, + MessageReceiverError.duplicateControlMessage, + MessageReceiverError.selfSend: + break + + default: SNLog("Couldn't receive open group message due to error: \(error).") + } } } - catch { - switch error { - // Ignore duplicate & selfSend message errors (and don't bother logging - // them as there will be a lot since we each service node duplicates messages) - case DatabaseError.SQLITE_CONSTRAINT_UNIQUE, - MessageReceiverError.duplicateMessage, - MessageReceiverError.duplicateControlMessage, - MessageReceiverError.selfSend: - break + + // Handle reactions + if message.reactions != nil { + do { + let reactions: [Reaction] = Message.processRawReceivedReactions( + db, + openGroupId: openGroup.id, + message: message, + associatedPendingChanges: dependencies.cache.pendingChanges + .filter { + guard $0.server == server && $0.room == roomToken && $0.changeType == .reaction else { + return false + } + + if case .reaction(let messageId, _, _) = $0.metadata { + return messageId == message.id + } + return false + }, + dependencies: dependencies + ) - default: SNLog("Couldn't receive open group message due to error: \(error).") + try MessageReceiver.handleOpenGroupReactions( + db, + threadId: openGroup.threadId, + openGroupMessageServerId: message.id, + openGroupReactions: reactions + ) + } + catch { + SNLog("Couldn't handle open group reactions due to error: \(error).") } } } @@ -588,6 +630,7 @@ public final class OpenGroupManager: NSObject { guard !messageServerIdsToRemove.isEmpty else { return } _ = try? Interaction + .filter(Interaction.Columns.threadId == openGroup.threadId) .filter(messageServerIdsToRemove.contains(Interaction.Columns.openGroupServerMessageId)) .deleteAll(db) } @@ -597,7 +640,6 @@ public final class OpenGroupManager: NSObject { messages: [OpenGroupAPI.DirectMessage], fromOutbox: Bool, on server: String, - isBackgroundPoll: Bool, dependencies: OGMDependencies = OGMDependencies() ) { // Don't need to do anything if we have no messages (it's a valid case) @@ -694,7 +736,6 @@ public final class OpenGroupManager: NSObject { message: messageInfo.message, associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData), openGroupId: nil, // Intentionally nil as they are technically not open group messages - isBackgroundPoll: isBackgroundPoll, dependencies: dependencies ) } @@ -718,6 +759,78 @@ public final class OpenGroupManager: NSObject { // MARK: - Convenience + public static func addPendingReaction( + emoji: String, + id: Int64, + in roomToken: String, + on server: String, + type: OpenGroupAPI.PendingChange.ReactAction, + using dependencies: OGMDependencies = OGMDependencies() + ) -> OpenGroupAPI.PendingChange { + let pendingChange = OpenGroupAPI.PendingChange( + server: server, + room: roomToken, + changeType: .reaction, + metadata: .reaction( + messageId: id, + emoji: emoji, + action: type + ) + ) + + dependencies.mutableCache.mutate { + $0.pendingChanges.append(pendingChange) + } + + return pendingChange + } + + public static func updatePendingChange( + _ pendingChange: OpenGroupAPI.PendingChange, + seqNo: Int64?, + using dependencies: OGMDependencies = OGMDependencies() + ) { + dependencies.mutableCache.mutate { + if let index = $0.pendingChanges.firstIndex(of: pendingChange) { + $0.pendingChanges[index].seqNo = seqNo + } + } + } + + public static func removePendingChange( + _ pendingChange: OpenGroupAPI.PendingChange, + using dependencies: OGMDependencies = OGMDependencies() + ) { + dependencies.mutableCache.mutate { + if let index = $0.pendingChanges.firstIndex(of: pendingChange) { + $0.pendingChanges.remove(at: index) + } + } + } + + /// This method specifies if the given capability is supported on a specified Open Group + public static func isOpenGroupSupport( + _ capability: Capability.Variant, + on server: String?, + using dependencies: OGMDependencies = OGMDependencies() + ) -> Bool { + guard let server: String = server else { return false } + + return dependencies.storage + .read { db in + let capabilities: [Capability.Variant] = (try? Capability + .select(.variant) + .filter(Capability.Columns.openGroupServer == server) + .filter(Capability.Columns.isMissing == false) + .asRequest(of: Capability.Variant.self) + .fetchAll(db)) + .defaulting(to: []) + + return capabilities.contains(capability) + } + .defaulting(to: false) + } + /// This method specifies if the given publicKey is a moderator or an admin within a specified Open Group public static func isUserModeratorOrAdmin( _ publicKey: String, @@ -997,10 +1110,10 @@ public final class OpenGroupManager: NSObject { extension OpenGroupManager { public class OGMDependencies: SMKDependencies { - internal var _mutableCache: Atomic? + internal var _mutableCache: Atomic?> public var mutableCache: Atomic { get { Dependencies.getValueSettingIfNull(&_mutableCache) { OpenGroupManager.shared.mutableCache } } - set { _mutableCache = newValue } + set { _mutableCache.mutate { $0 = newValue } } } public var cache: OGMCacheType { return mutableCache.wrappedValue } @@ -1021,7 +1134,7 @@ extension OpenGroupManager { standardUserDefaults: UserDefaultsType? = nil, date: Date? = nil ) { - _mutableCache = cache + _mutableCache = Atomic(cache) super.init( onionApi: onionApi, diff --git a/SessionMessagingKit/Open Groups/Types/OpenGroupAPIError.swift b/SessionMessagingKit/Open Groups/Types/OpenGroupAPIError.swift index b09b90d61..fa427f86f 100644 --- a/SessionMessagingKit/Open Groups/Types/OpenGroupAPIError.swift +++ b/SessionMessagingKit/Open Groups/Types/OpenGroupAPIError.swift @@ -6,12 +6,14 @@ public enum OpenGroupAPIError: LocalizedError { case decryptionFailed case signingFailed case noPublicKey + case invalidEmoji public var errorDescription: String? { switch self { case .decryptionFailed: return "Couldn't decrypt response." case .signingFailed: return "Couldn't sign message." case .noPublicKey: return "Couldn't find server public key." + case .invalidEmoji: return "The emoji is invalid." } } } diff --git a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift index 052b2fe80..60c148595 100644 --- a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift +++ b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift @@ -26,6 +26,12 @@ extension OpenGroupAPI { case roomMessagesSince(String, seqNo: Int64) case roomDeleteMessages(String, sessionId: String) + // Reactions + + case reactionDelete(String, id: Int64, emoji: String) + case reaction(String, id: Int64, emoji: String) + case reactors(String, id: Int64, emoji: String) + // Pinning case roomPinMessage(String, id: Int64) @@ -86,6 +92,17 @@ extension OpenGroupAPI { case .roomDeleteMessages(let roomToken, let sessionId): return "room/\(roomToken)/all/\(sessionId)" + + // Reactions + + case .reactionDelete(let roomToken, let messageId, let emoji): + return "room/\(roomToken)/reactions/\(messageId)/\(emoji)" + + case .reaction(let roomToken, let messageId, let emoji): + return "room/\(roomToken)/reaction/\(messageId)/\(emoji)" + + case .reactors(let roomToken, let messageId, let emoji): + return "room/\(roomToken)/reactors/\(messageId)/\(emoji)" // Pinning diff --git a/SessionMessagingKit/Protos/Generated/SNProto.swift b/SessionMessagingKit/Protos/Generated/SNProto.swift index 457848361..a575ba288 100644 --- a/SessionMessagingKit/Protos/Generated/SNProto.swift +++ b/SessionMessagingKit/Protos/Generated/SNProto.swift @@ -1633,6 +1633,171 @@ extension SNProtoDataMessagePreview.SNProtoDataMessagePreviewBuilder { #endif +// MARK: - SNProtoDataMessageReaction + +@objc public class SNProtoDataMessageReaction: NSObject { + + // MARK: - SNProtoDataMessageReactionAction + + @objc public enum SNProtoDataMessageReactionAction: Int32 { + case react = 0 + case remove = 1 + } + + private class func SNProtoDataMessageReactionActionWrap(_ value: SessionProtos_DataMessage.Reaction.Action) -> SNProtoDataMessageReactionAction { + switch value { + case .react: return .react + case .remove: return .remove + } + } + + private class func SNProtoDataMessageReactionActionUnwrap(_ value: SNProtoDataMessageReactionAction) -> SessionProtos_DataMessage.Reaction.Action { + switch value { + case .react: return .react + case .remove: return .remove + } + } + + // MARK: - SNProtoDataMessageReactionBuilder + + @objc public class func builder(id: UInt64, author: String, action: SNProtoDataMessageReactionAction) -> SNProtoDataMessageReactionBuilder { + return SNProtoDataMessageReactionBuilder(id: id, author: author, action: action) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SNProtoDataMessageReactionBuilder { + let builder = SNProtoDataMessageReactionBuilder(id: id, author: author, action: action) + if let _value = emoji { + builder.setEmoji(_value) + } + return builder + } + + @objc public class SNProtoDataMessageReactionBuilder: NSObject { + + private var proto = SessionProtos_DataMessage.Reaction() + + @objc fileprivate override init() {} + + @objc fileprivate init(id: UInt64, author: String, action: SNProtoDataMessageReactionAction) { + super.init() + + setId(id) + setAuthor(author) + setAction(action) + } + + @objc public func setId(_ valueParam: UInt64) { + proto.id = valueParam + } + + @objc public func setAuthor(_ valueParam: String) { + proto.author = valueParam + } + + @objc public func setEmoji(_ valueParam: String) { + proto.emoji = valueParam + } + + @objc public func setAction(_ valueParam: SNProtoDataMessageReactionAction) { + proto.action = SNProtoDataMessageReactionActionUnwrap(valueParam) + } + + @objc public func build() throws -> SNProtoDataMessageReaction { + return try SNProtoDataMessageReaction.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SNProtoDataMessageReaction.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SessionProtos_DataMessage.Reaction + + @objc public let id: UInt64 + + @objc public let author: String + + @objc public let action: SNProtoDataMessageReactionAction + + @objc public var emoji: String? { + guard proto.hasEmoji else { + return nil + } + return proto.emoji + } + @objc public var hasEmoji: Bool { + return proto.hasEmoji + } + + private init(proto: SessionProtos_DataMessage.Reaction, + id: UInt64, + author: String, + action: SNProtoDataMessageReactionAction) { + self.proto = proto + self.id = id + self.author = author + self.action = action + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SNProtoDataMessageReaction { + let proto = try SessionProtos_DataMessage.Reaction(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SessionProtos_DataMessage.Reaction) throws -> SNProtoDataMessageReaction { + guard proto.hasID else { + throw SNProtoError.invalidProtobuf(description: "\(logTag) missing required field: id") + } + let id = proto.id + + guard proto.hasAuthor else { + throw SNProtoError.invalidProtobuf(description: "\(logTag) missing required field: author") + } + let author = proto.author + + guard proto.hasAction else { + throw SNProtoError.invalidProtobuf(description: "\(logTag) missing required field: action") + } + let action = SNProtoDataMessageReactionActionWrap(proto.action) + + // MARK: - Begin Validation Logic for SNProtoDataMessageReaction - + + // MARK: - End Validation Logic for SNProtoDataMessageReaction - + + let result = SNProtoDataMessageReaction(proto: proto, + id: id, + author: author, + action: action) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SNProtoDataMessageReaction { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SNProtoDataMessageReaction.SNProtoDataMessageReactionBuilder { + @objc public func buildIgnoringErrors() -> SNProtoDataMessageReaction? { + return try! self.build() + } +} + +#endif + // MARK: - SNProtoDataMessageLokiProfile @objc public class SNProtoDataMessageLokiProfile: NSObject { @@ -2269,6 +2434,9 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr builder.setQuote(_value) } builder.setPreview(preview) + if let _value = reaction { + builder.setReaction(_value) + } if let _value = profile { builder.setProfile(_value) } @@ -2338,6 +2506,10 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr proto.preview = wrappedItems.map { $0.proto } } + @objc public func setReaction(_ valueParam: SNProtoDataMessageReaction) { + proto.reaction = valueParam.proto + } + @objc public func setProfile(_ valueParam: SNProtoDataMessageLokiProfile) { proto.profile = valueParam.proto } @@ -2373,6 +2545,8 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr @objc public let preview: [SNProtoDataMessagePreview] + @objc public let reaction: SNProtoDataMessageReaction? + @objc public let profile: SNProtoDataMessageLokiProfile? @objc public let openGroupInvitation: SNProtoDataMessageOpenGroupInvitation? @@ -2435,6 +2609,7 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr group: SNProtoGroupContext?, quote: SNProtoDataMessageQuote?, preview: [SNProtoDataMessagePreview], + reaction: SNProtoDataMessageReaction?, profile: SNProtoDataMessageLokiProfile?, openGroupInvitation: SNProtoDataMessageOpenGroupInvitation?, closedGroupControlMessage: SNProtoDataMessageClosedGroupControlMessage?) { @@ -2443,6 +2618,7 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr self.group = group self.quote = quote self.preview = preview + self.reaction = reaction self.profile = profile self.openGroupInvitation = openGroupInvitation self.closedGroupControlMessage = closedGroupControlMessage @@ -2475,6 +2651,11 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr var preview: [SNProtoDataMessagePreview] = [] preview = try proto.preview.map { try SNProtoDataMessagePreview.parseProto($0) } + var reaction: SNProtoDataMessageReaction? = nil + if proto.hasReaction { + reaction = try SNProtoDataMessageReaction.parseProto(proto.reaction) + } + var profile: SNProtoDataMessageLokiProfile? = nil if proto.hasProfile { profile = try SNProtoDataMessageLokiProfile.parseProto(proto.profile) @@ -2499,6 +2680,7 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr group: group, quote: quote, preview: preview, + reaction: reaction, profile: profile, openGroupInvitation: openGroupInvitation, closedGroupControlMessage: closedGroupControlMessage) diff --git a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift index 854fd1ad2..77cbc650f 100644 --- a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift +++ b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift @@ -599,6 +599,15 @@ struct SessionProtos_DataMessage { set {_uniqueStorage()._preview = newValue} } + var reaction: SessionProtos_DataMessage.Reaction { + get {return _storage._reaction ?? SessionProtos_DataMessage.Reaction()} + set {_uniqueStorage()._reaction = newValue} + } + /// Returns true if `reaction` has been explicitly set. + var hasReaction: Bool {return _storage._reaction != nil} + /// Clears the value of `reaction`. Subsequent reads from it will return its default value. + mutating func clearReaction() {_uniqueStorage()._reaction = nil} + var profile: SessionProtos_DataMessage.LokiProfile { get {return _storage._profile ?? SessionProtos_DataMessage.LokiProfile()} set {_uniqueStorage()._profile = newValue} @@ -821,6 +830,86 @@ struct SessionProtos_DataMessage { fileprivate var _image: SessionProtos_AttachmentPointer? = nil } + struct Reaction { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var id: UInt64 { + get {return _id ?? 0} + set {_id = newValue} + } + /// Returns true if `id` has been explicitly set. + var hasID: Bool {return self._id != nil} + /// Clears the value of `id`. Subsequent reads from it will return its default value. + mutating func clearID() {self._id = nil} + + /// @required + var author: String { + get {return _author ?? String()} + set {_author = newValue} + } + /// Returns true if `author` has been explicitly set. + var hasAuthor: Bool {return self._author != nil} + /// Clears the value of `author`. Subsequent reads from it will return its default value. + mutating func clearAuthor() {self._author = nil} + + var emoji: String { + get {return _emoji ?? String()} + set {_emoji = newValue} + } + /// Returns true if `emoji` has been explicitly set. + var hasEmoji: Bool {return self._emoji != nil} + /// Clears the value of `emoji`. Subsequent reads from it will return its default value. + mutating func clearEmoji() {self._emoji = nil} + + /// @required + var action: SessionProtos_DataMessage.Reaction.Action { + get {return _action ?? .react} + set {_action = newValue} + } + /// Returns true if `action` has been explicitly set. + var hasAction: Bool {return self._action != nil} + /// Clears the value of `action`. Subsequent reads from it will return its default value. + mutating func clearAction() {self._action = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum Action: SwiftProtobuf.Enum { + typealias RawValue = Int + case react // = 0 + case remove // = 1 + + init() { + self = .react + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .react + case 1: self = .remove + default: return nil + } + } + + var rawValue: Int { + switch self { + case .react: return 0 + case .remove: return 1 + } + } + + } + + init() {} + + fileprivate var _id: UInt64? = nil + fileprivate var _author: String? = nil + fileprivate var _emoji: String? = nil + fileprivate var _action: SessionProtos_DataMessage.Reaction.Action? = nil + } + struct LokiProfile { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -1052,6 +1141,10 @@ extension SessionProtos_DataMessage.Quote.QuotedAttachment.Flags: CaseIterable { // Support synthesized by the compiler. } +extension SessionProtos_DataMessage.Reaction.Action: CaseIterable { + // Support synthesized by the compiler. +} + extension SessionProtos_DataMessage.ClosedGroupControlMessage.TypeEnum: CaseIterable { // Support synthesized by the compiler. } @@ -2094,6 +2187,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa 7: .same(proto: "timestamp"), 8: .same(proto: "quote"), 10: .same(proto: "preview"), + 11: .same(proto: "reaction"), 101: .same(proto: "profile"), 102: .same(proto: "openGroupInvitation"), 104: .same(proto: "closedGroupControlMessage"), @@ -2110,6 +2204,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa var _timestamp: UInt64? = nil var _quote: SessionProtos_DataMessage.Quote? = nil var _preview: [SessionProtos_DataMessage.Preview] = [] + var _reaction: SessionProtos_DataMessage.Reaction? = nil var _profile: SessionProtos_DataMessage.LokiProfile? = nil var _openGroupInvitation: SessionProtos_DataMessage.OpenGroupInvitation? = nil var _closedGroupControlMessage: SessionProtos_DataMessage.ClosedGroupControlMessage? = nil @@ -2129,6 +2224,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa _timestamp = source._timestamp _quote = source._quote _preview = source._preview + _reaction = source._reaction _profile = source._profile _openGroupInvitation = source._openGroupInvitation _closedGroupControlMessage = source._closedGroupControlMessage @@ -2149,6 +2245,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa if let v = _storage._group, !v.isInitialized {return false} if let v = _storage._quote, !v.isInitialized {return false} if !SwiftProtobuf.Internal.areAllInitialized(_storage._preview) {return false} + if let v = _storage._reaction, !v.isInitialized {return false} if let v = _storage._openGroupInvitation, !v.isInitialized {return false} if let v = _storage._closedGroupControlMessage, !v.isInitialized {return false} return true @@ -2172,6 +2269,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa case 7: try { try decoder.decodeSingularUInt64Field(value: &_storage._timestamp) }() case 8: try { try decoder.decodeSingularMessageField(value: &_storage._quote) }() case 10: try { try decoder.decodeRepeatedMessageField(value: &_storage._preview) }() + case 11: try { try decoder.decodeSingularMessageField(value: &_storage._reaction) }() case 101: try { try decoder.decodeSingularMessageField(value: &_storage._profile) }() case 102: try { try decoder.decodeSingularMessageField(value: &_storage._openGroupInvitation) }() case 104: try { try decoder.decodeSingularMessageField(value: &_storage._closedGroupControlMessage) }() @@ -2211,6 +2309,9 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa if !_storage._preview.isEmpty { try visitor.visitRepeatedMessageField(value: _storage._preview, fieldNumber: 10) } + if let v = _storage._reaction { + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + } if let v = _storage._profile { try visitor.visitSingularMessageField(value: v, fieldNumber: 101) } @@ -2241,6 +2342,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa if _storage._timestamp != rhs_storage._timestamp {return false} if _storage._quote != rhs_storage._quote {return false} if _storage._preview != rhs_storage._preview {return false} + if _storage._reaction != rhs_storage._reaction {return false} if _storage._profile != rhs_storage._profile {return false} if _storage._openGroupInvitation != rhs_storage._openGroupInvitation {return false} if _storage._closedGroupControlMessage != rhs_storage._closedGroupControlMessage {return false} @@ -2428,6 +2530,70 @@ extension SessionProtos_DataMessage.Preview: SwiftProtobuf.Message, SwiftProtobu } } +extension SessionProtos_DataMessage.Reaction: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SessionProtos_DataMessage.protoMessageName + ".Reaction" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "id"), + 2: .same(proto: "author"), + 3: .same(proto: "emoji"), + 4: .same(proto: "action"), + ] + + public var isInitialized: Bool { + if self._id == nil {return false} + if self._author == nil {return false} + if self._action == nil {return false} + return true + } + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self._id) }() + case 2: try { try decoder.decodeSingularStringField(value: &self._author) }() + case 3: try { try decoder.decodeSingularStringField(value: &self._emoji) }() + case 4: try { try decoder.decodeSingularEnumField(value: &self._action) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._id { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1) + } + if let v = self._author { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } + if let v = self._emoji { + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + } + if let v = self._action { + try visitor.visitSingularEnumField(value: v, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SessionProtos_DataMessage.Reaction, rhs: SessionProtos_DataMessage.Reaction) -> Bool { + if lhs._id != rhs._id {return false} + if lhs._author != rhs._author {return false} + if lhs._emoji != rhs._emoji {return false} + if lhs._action != rhs._action {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SessionProtos_DataMessage.Reaction.Action: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "REACT"), + 1: .same(proto: "REMOVE"), + ] +} + extension SessionProtos_DataMessage.LokiProfile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = SessionProtos_DataMessage.protoMessageName + ".LokiProfile" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ diff --git a/SessionMessagingKit/Protos/SessionProtos.proto b/SessionMessagingKit/Protos/SessionProtos.proto index 1f3962c55..88f1fc115 100644 --- a/SessionMessagingKit/Protos/SessionProtos.proto +++ b/SessionMessagingKit/Protos/SessionProtos.proto @@ -133,6 +133,20 @@ message DataMessage { optional AttachmentPointer image = 3; } + message Reaction { + enum Action { + REACT = 0; + REMOVE = 1; + } + // @required + required uint64 id = 1; // Message timestamp + // @required + required string author = 2; + optional string emoji = 3; + // @required + required Action action = 4; + } + message LokiProfile { optional string displayName = 1; optional string profilePicture = 2; @@ -184,6 +198,7 @@ message DataMessage { optional uint64 timestamp = 7; optional Quote quote = 8; repeated Preview preview = 10; + optional Reaction reaction = 11; optional LokiProfile profile = 101; optional OpenGroupInvitation openGroupInvitation = 102; optional ClosedGroupControlMessage closedGroupControlMessage = 104; diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift index 4b1766a0a..fd74915a9 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift @@ -21,7 +21,11 @@ extension MessageReceiver { case .screenshot: return .infoScreenshotNotification case .mediaSaved: return .infoMediaSavedNotification } - }() + }(), + timestampMs: ( + message.sentTimestamp.map { Int64($0) } ?? + Int64(floor(Date().timeIntervalSince1970 * 1000)) + ) ).inserted(db) } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index a18e0560b..10018d5e1 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -12,7 +12,6 @@ extension MessageReceiver { message: VisibleMessage, associatedWithProto proto: SNProtoContent, openGroupId: String?, - isBackgroundPoll: Bool, dependencies: Dependencies = Dependencies() ) throws -> Int64 { guard let sender: String = message.sender, let dataMessage = proto.dataMessage else { @@ -88,6 +87,11 @@ extension MessageReceiver { } }() + // Handle emoji reacts first (otherwise it's essentially an invalid message) + if let interactionId: Int64 = try handleEmojiReactIfNeeded(db, message: message, associatedWithProto: proto, sender: sender, messageSentTimestamp: messageSentTimestamp, openGroupId: openGroupId, thread: thread) { + return interactionId + } + // Retrieve the disappearing messages config to set the 'expiresInSeconds' value // accoring to the config let disappearingMessagesConfiguration: DisappearingMessagesConfiguration = (try? thread.disappearingMessagesConfiguration.fetchOne(db)) @@ -285,13 +289,75 @@ extension MessageReceiver { .notifyUser( db, for: interaction, - in: thread, - isBackgroundPoll: isBackgroundPoll + in: thread ) return interactionId } + private static func handleEmojiReactIfNeeded( + _ db: Database, + message: VisibleMessage, + associatedWithProto proto: SNProtoContent, + sender: String, + messageSentTimestamp: TimeInterval, + openGroupId: String?, + thread: SessionThread + ) throws -> Int64? { + guard + let reaction: VisibleMessage.VMReaction = message.reaction, + proto.dataMessage?.reaction != nil + else { return nil } + + let maybeInteractionId: Int64? = try? Interaction + .select(.id) + .filter(Interaction.Columns.threadId == thread.id) + .filter(Interaction.Columns.timestampMs == reaction.timestamp) + .filter(Interaction.Columns.authorId == reaction.publicKey) + .filter(Interaction.Columns.variant != Interaction.Variant.standardIncomingDeleted) + .asRequest(of: Int64.self) + .fetchOne(db) + + guard let interactionId: Int64 = maybeInteractionId else { + throw StorageError.objectNotFound + } + + let sortId = Reaction.getSortId( + db, + interactionId: interactionId, + emoji: reaction.emoji + ) + + switch reaction.kind { + case .react: + let reaction = Reaction( + interactionId: interactionId, + serverHash: message.serverHash, + timestampMs: Int64(messageSentTimestamp * 1000), + authorId: sender, + emoji: reaction.emoji, + count: 1, + sortId: sortId + ) + try reaction.insert(db) + Environment.shared?.notificationsManager.wrappedValue? + .notifyUser( + db, + forReaction: reaction, + in: thread + ) + + case .remove: + try Reaction + .filter(Reaction.Columns.interactionId == interactionId) + .filter(Reaction.Columns.authorId == sender) + .filter(Reaction.Columns.emoji == reaction.emoji) + .deleteAll(db) + } + + return interactionId + } + private static func updateRecipientAndReadStates( _ db: Database, thread: SessionThread, diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 4524b75d2..2e427fda1 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -180,7 +180,6 @@ public enum MessageReceiver { message: Message, associatedWithProto proto: SNProtoContent, openGroupId: String?, - isBackgroundPoll: Bool, dependencies: SMKDependencies = SMKDependencies() ) throws { switch message { @@ -206,7 +205,7 @@ public enum MessageReceiver { try MessageReceiver.handleUnsendRequest(db, message: message) case let message as CallMessage: - try MessageReceiver.handleCallMessage(db, message: message) + try MessageReceiver.handleCallMessage(db, message: message) case let message as MessageRequestResponse: try MessageReceiver.handleMessageRequestResponse(db, message: message, dependencies: dependencies) @@ -216,8 +215,7 @@ public enum MessageReceiver { db, message: message, associatedWithProto: proto, - openGroupId: openGroupId, - isBackgroundPoll: isBackgroundPoll + openGroupId: openGroupId ) default: fatalError() @@ -249,6 +247,31 @@ public enum MessageReceiver { } } + public static func handleOpenGroupReactions( + _ db: Database, + threadId: String, + openGroupMessageServerId: Int64, + openGroupReactions: [Reaction] + ) throws { + guard let interactionId: Int64 = try? Interaction + .select(.id) + .filter(Interaction.Columns.threadId == threadId) + .filter(Interaction.Columns.openGroupServerMessageId == openGroupMessageServerId) + .asRequest(of: Int64.self) + .fetchOne(db) + else { + throw MessageReceiverError.invalidMessage + } + + _ = try Reaction + .filter(Reaction.Columns.interactionId == interactionId) + .deleteAll(db) + + for reaction in openGroupReactions { + try reaction.with(interactionId: interactionId).insert(db) + } + } + // MARK: - Convenience internal static func threadInfo(_ db: Database, message: Message, openGroupId: String?) -> (id: String, variant: SessionThread.Variant)? { diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 438ffa55f..cd61d1169 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -433,7 +433,8 @@ public final class MessageSender { ) .done(on: DispatchQueue.global(qos: .default)) { responseInfo, data in message.openGroupServerMessageId = UInt64(data.id) - + let serverTimestampMs: UInt64? = data.posted.map { UInt64(floor($0 * 1000)) } + dependencies.storage.write { db in // The `posted` value is in seconds but we sent it in ms so need that for de-duping try MessageSender.handleSuccessfulMessageSend( @@ -441,7 +442,7 @@ public final class MessageSender { message: message, to: destination, interactionId: interactionId, - serverTimestampMs: UInt64(floor(data.posted * 1000)) + serverTimestampMs: serverTimestampMs ) seal.fulfill(()) } @@ -575,39 +576,51 @@ public final class MessageSender { serverTimestampMs: UInt64? = nil, isSyncMessage: Bool = false ) throws { - let interaction: Interaction? = try interaction(db, for: message, interactionId: interactionId) - - // Get the visible message if possible - if let interaction: Interaction = interaction { - // When the sync message is successfully sent, the hash value of this TSOutgoingMessage - // will be replaced by the hash value of the sync message. Since the hash value of the - // real message has no use when we delete a message. It is OK to let it be. - try interaction.with( - serverHash: message.serverHash, + // If the message was a reaction then we want to update the reaction instead of the original + // interaciton (which the 'interactionId' is pointing to + if let visibleMessage: VisibleMessage = message as? VisibleMessage, let reaction: VisibleMessage.VMReaction = visibleMessage.reaction { + try Reaction + .filter(Reaction.Columns.interactionId == interactionId) + .filter(Reaction.Columns.authorId == reaction.publicKey) + .filter(Reaction.Columns.emoji == reaction.emoji) + .updateAll(db, Reaction.Columns.serverHash.set(to: message.serverHash)) + } + else { + // Otherwise we do want to try and update the referenced interaction + let interaction: Interaction? = try interaction(db, for: message, interactionId: interactionId) + + // Get the visible message if possible + if let interaction: Interaction = interaction { + // When the sync message is successfully sent, the hash value of this TSOutgoingMessage + // will be replaced by the hash value of the sync message. Since the hash value of the + // real message has no use when we delete a message. It is OK to let it be. + try interaction.with( + serverHash: message.serverHash, + + // Track the open group server message ID and update server timestamp (use server + // timestamp for open group messages otherwise the quote messages may not be able + // to be found by the timestamp on other devices + timestampMs: (message.openGroupServerMessageId == nil ? + nil : + serverTimestampMs.map { Int64($0) } + ), + openGroupServerMessageId: message.openGroupServerMessageId.map { Int64($0) } + ).update(db) - // Track the open group server message ID and update server timestamp (use server - // timestamp for open group messages otherwise the quote messages may not be able - // to be found by the timestamp on other devices - timestampMs: (message.openGroupServerMessageId == nil ? - nil : - serverTimestampMs.map { Int64($0) } - ), - openGroupServerMessageId: message.openGroupServerMessageId.map { Int64($0) } - ).update(db) - - // Mark the message as sent - try interaction.recipientStates - .updateAll(db, RecipientState.Columns.state.set(to: RecipientState.State.sent)) - - // Start the disappearing messages timer if needed - JobRunner.upsert( - db, - job: DisappearingMessagesJob.updateNextRunIfNeeded( + // Mark the message as sent + try interaction.recipientStates + .updateAll(db, RecipientState.Columns.state.set(to: RecipientState.State.sent)) + + // Start the disappearing messages timer if needed + JobRunner.upsert( db, - interaction: interaction, - startedAtMs: (Date().timeIntervalSince1970 * 1000) + job: DisappearingMessagesJob.updateNextRunIfNeeded( + db, + interaction: interaction, + startedAtMs: (Date().timeIntervalSince1970 * 1000) + ) ) - ) + } } // Prevent ControlMessages from being handled multiple times if not supported @@ -652,6 +665,11 @@ public final class MessageSender { with error: MessageSenderError, interactionId: Int64? ) { + // TODO: Revert the local database change + // If the message was a reaction then we don't want to do anything to the original + // interaciton (which the 'interactionId' is pointing to + guard (message as? VisibleMessage)?.reaction == nil else { return } + // Check if we need to mark any "sending" recipients as "failed" // // Note: The 'db' could be either read-only or writeable so we determine diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.swift b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.swift index 0fefd991f..51e2951ac 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.swift @@ -4,8 +4,15 @@ import Foundation import GRDB public protocol NotificationsProtocol { - func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, isBackgroundPoll: Bool) + func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread) func notifyUser(_ db: Database, forIncomingCall interaction: Interaction, in thread: SessionThread) + func notifyUser(_ db: Database, forReaction reaction: Reaction, in thread: SessionThread) func cancelNotifications(identifiers: [String]) func clearAllNotifications() } + +public enum Notifications { + /// Delay notification of incoming messages when we want to group them (eg. during background polling) to avoid + /// firing too many notifications at the same time + public static let delayForGroupedNotifications: TimeInterval = 5 +} diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift index e87643a1b..40fccd4e4 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift @@ -145,7 +145,7 @@ public final class ClosedGroupPoller { _ groupPublicKey: String, on queue: DispatchQueue = SessionSnodeKit.Threading.workQueue, maxRetryCount: UInt = 0, - isBackgroundPoll: Bool = false, + calledFromBackgroundPoller: Bool = false, isBackgroundPollValid: @escaping (() -> Bool) = { true }, poller: ClosedGroupPoller? = nil ) -> Promise { @@ -156,7 +156,7 @@ public final class ClosedGroupPoller { return attempt(maxRetryCount: maxRetryCount, recoveringOn: queue) { guard - (isBackgroundPoll && isBackgroundPollValid()) || + (calledFromBackgroundPoller && isBackgroundPollValid()) || poller?.isPolling.wrappedValue[groupPublicKey] == true else { return Promise(error: Error.pollingCanceled) } @@ -178,7 +178,7 @@ public final class ClosedGroupPoller { return when(resolved: promises) .then(on: queue) { messageResults -> Promise in guard - (isBackgroundPoll && isBackgroundPollValid()) || + (calledFromBackgroundPoller && isBackgroundPollValid()) || poller?.isPolling.wrappedValue[groupPublicKey] == true else { return Promise.value(()) } @@ -195,7 +195,7 @@ public final class ClosedGroupPoller { // No need to do anything if there are no messages guard !allMessages.isEmpty else { - if !isBackgroundPoll { + if !calledFromBackgroundPoller { SNLog("Received no new messages in closed group with public key: \(groupPublicKey)") } return Promise.value(()) @@ -221,7 +221,7 @@ public final class ClosedGroupPoller { // In the background ignore 'SQLITE_ABORT' (it generally means // the BackgroundPoller has timed out case DatabaseError.SQLITE_ABORT: - guard !isBackgroundPoll else { break } + guard !calledFromBackgroundPoller else { break } SNLog("Failed to the database being suspended (running in background with no background task).") break @@ -241,16 +241,16 @@ public final class ClosedGroupPoller { threadId: groupPublicKey, details: MessageReceiveJob.Details( messages: processedMessages.map { $0.messageInfo }, - isBackgroundPoll: isBackgroundPoll + calledFromBackgroundPoller: calledFromBackgroundPoller ) ) // If we are force-polling then add to the JobRunner so they are persistent and will retry on // the next app run if they fail but don't let them auto-start - JobRunner.add(db, job: jobToRun, canStartJob: !isBackgroundPoll) + JobRunner.add(db, job: jobToRun, canStartJob: !calledFromBackgroundPoller) } - if isBackgroundPoll { + if calledFromBackgroundPoller { // We want to try to handle the receive jobs immediately in the background promises = promises.appending( jobToRun.map { job -> Promise in @@ -278,7 +278,7 @@ public final class ClosedGroupPoller { } } - if !isBackgroundPoll { + if !calledFromBackgroundPoller { promise.catch2 { error in SNLog("Polling failed for closed group with public key: \(groupPublicKey) due to error: \(error).") } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift index 7f270a4c4..4a83d07b6 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift @@ -67,12 +67,12 @@ extension OpenGroupAPI { @discardableResult public func poll(using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()) -> Promise { - return poll(isBackgroundPoll: false, isPostCapabilitiesRetry: false, using: dependencies) + return poll(calledFromBackgroundPoller: false, isPostCapabilitiesRetry: false, using: dependencies) } @discardableResult public func poll( - isBackgroundPoll: Bool, + calledFromBackgroundPoller: Bool, isBackgroundPollerValid: @escaping (() -> Bool) = { true }, isPostCapabilitiesRetry: Bool, using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies() @@ -107,7 +107,7 @@ extension OpenGroupAPI { .map(on: OpenGroupAPI.workQueue) { (failureCount, $0) } } .done(on: OpenGroupAPI.workQueue) { [weak self] failureCount, response in - guard !isBackgroundPoll || isBackgroundPollerValid() else { + guard !calledFromBackgroundPoller || isBackgroundPollerValid() else { // If this was a background poll and the background poll is no longer valid // then just stop self?.isPolling = false @@ -119,7 +119,6 @@ extension OpenGroupAPI { self?.handlePollResponse( response, failureCount: failureCount, - isBackgroundPoll: isBackgroundPoll, using: dependencies ) @@ -133,7 +132,7 @@ extension OpenGroupAPI { seal.fulfill(()) } .catch(on: OpenGroupAPI.workQueue) { [weak self] error in - guard !isBackgroundPoll || isBackgroundPollerValid() else { + guard !calledFromBackgroundPoller || isBackgroundPollerValid() else { // If this was a background poll and the background poll is no longer valid // then just stop self?.isPolling = false @@ -145,7 +144,8 @@ extension OpenGroupAPI { // method will always resolve) self?.updateCapabilitiesAndRetryIfNeeded( server: server, - isBackgroundPoll: isBackgroundPoll, + calledFromBackgroundPoller: calledFromBackgroundPoller, + isBackgroundPollerValid: isBackgroundPollerValid, isPostCapabilitiesRetry: isPostCapabilitiesRetry, error: error ) @@ -186,7 +186,8 @@ extension OpenGroupAPI { private func updateCapabilitiesAndRetryIfNeeded( server: String, - isBackgroundPoll: Bool, + calledFromBackgroundPoller: Bool, + isBackgroundPollerValid: @escaping (() -> Bool) = { true }, isPostCapabilitiesRetry: Bool, error: Error, using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies() @@ -233,7 +234,8 @@ extension OpenGroupAPI { // Regardless of the outcome we can just resolve this // immediately as it'll handle it's own response return strongSelf.poll( - isBackgroundPoll: isBackgroundPoll, + calledFromBackgroundPoller: calledFromBackgroundPoller, + isBackgroundPollerValid: isBackgroundPollerValid, isPostCapabilitiesRetry: true, using: dependencies ) @@ -251,7 +253,6 @@ extension OpenGroupAPI { private func handlePollResponse( _ response: PollResponse, failureCount: Int64, - isBackgroundPoll: Bool, using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies() ) { let server: String = self.server @@ -440,7 +441,6 @@ extension OpenGroupAPI { messages: responseBody.compactMap { $0.value }, for: roomToken, on: server, - isBackgroundPoll: isBackgroundPoll, dependencies: dependencies ) @@ -464,7 +464,6 @@ extension OpenGroupAPI { messages: messages, fromOutbox: fromOutbox, on: server, - isBackgroundPoll: isBackgroundPoll, dependencies: dependencies ) diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift index 4877077d2..4d6c3580a 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift @@ -89,7 +89,7 @@ public final class Poller { private func pollNextSnode(seal: Resolver) { let userPublicKey = getUserHexEncodedPublicKey() - let swarm = SnodeAPI.swarmCache[userPublicKey] ?? [] + let swarm = SnodeAPI.swarmCache.wrappedValue[userPublicKey] ?? [] let unusedSnodes = swarm.subtracting(usedSnodes) guard !unusedSnodes.isEmpty else { @@ -173,7 +173,7 @@ public final class Poller { threadId: threadId, details: MessageReceiveJob.Details( messages: threadMessages.map { $0.messageInfo }, - isBackgroundPoll: false + calledFromBackgroundPoller: false ) ) ) diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index bad4cb96e..861c82628 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -7,6 +7,7 @@ import SessionUtilitiesKit fileprivate typealias ViewModel = MessageViewModel fileprivate typealias AttachmentInteractionInfo = MessageViewModel.AttachmentInteractionInfo +fileprivate typealias ReactionInfo = MessageViewModel.ReactionInfo fileprivate typealias TypingIndicatorInfo = MessageViewModel.TypingIndicatorInfo public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable { @@ -100,6 +101,9 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, /// This value includes the associated attachments public let attachments: [Attachment]? + /// This value includes the associated reactions + public let reactionInfo: [ReactionInfo]? + /// This value defines what type of cell should appear and is generated based on the interaction variant /// and associated attachment data public let cellType: CellType @@ -141,7 +145,10 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, // MARK: - Mutation - public func with(attachments: [Attachment]) -> MessageViewModel { + public func with( + attachments: Updatable<[Attachment]> = .existing, + reactionInfo: Updatable<[ReactionInfo]> = .existing + ) -> MessageViewModel { return MessageViewModel( threadId: self.threadId, threadVariant: self.threadVariant, @@ -171,7 +178,8 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, linkPreview: self.linkPreview, linkPreviewAttachment: self.linkPreviewAttachment, currentUserPublicKey: self.currentUserPublicKey, - attachments: attachments, + attachments: (attachments ?? self.attachments), + reactionInfo: (reactionInfo ?? self.reactionInfo), cellType: self.cellType, authorName: self.authorName, senderName: self.senderName, @@ -349,6 +357,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, linkPreviewAttachment: self.linkPreviewAttachment, currentUserPublicKey: self.currentUserPublicKey, attachments: self.attachments, + reactionInfo: self.reactionInfo, cellType: cellType, authorName: authorDisplayName, senderName: { @@ -430,6 +439,37 @@ public extension MessageViewModel { } } +// MARK: - ReactionInfo + +public extension MessageViewModel { + struct ReactionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, Hashable, Differentiable { + public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) + public static let reactionKey: SQL = SQL(stringLiteral: CodingKeys.reaction.stringValue) + public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue) + + public static let reactionString: String = CodingKeys.reaction.stringValue + public static let profileString: String = CodingKeys.profile.stringValue + + public let rowId: Int64 + public let reaction: Reaction + public let profile: Profile? + + // MARK: - Identifiable + + public var differenceIdentifier: String { return id } + + public var id: String { + "\(reaction.emoji)-\(reaction.interactionId)-\(reaction.authorId)" + } + + // MARK: - Comparable + + public static func < (lhs: ReactionInfo, rhs: ReactionInfo) -> Bool { + return (lhs.reaction.sortId < rhs.reaction.sortId) + } + } +} + // MARK: - TypingIndicatorInfo public extension MessageViewModel { @@ -494,6 +534,7 @@ public extension MessageViewModel { // Post-Query Processing Data self.attachments = nil + self.reactionInfo = nil self.cellType = .typingIndicator self.authorName = "" self.senderName = nil @@ -596,6 +637,7 @@ public extension MessageViewModel { let attachmentIdColumnLiteral: SQL = SQL(stringLiteral: Attachment.Columns.id.name) let groupMemberModeratorTableLiteral: SQL = SQL(stringLiteral: "groupMemberModerator") let groupMemberAdminTableLiteral: SQL = SQL(stringLiteral: "groupMemberAdmin") + let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name) let groupMemberProfileIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.profileId.name) let groupMemberRoleColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.role.name) @@ -674,11 +716,13 @@ public extension MessageViewModel { ) LEFT JOIN \(GroupMember.self) AS \(groupMemberModeratorTableLiteral) ON ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND + \(groupMemberModeratorTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(interaction[.threadId]) AND \(groupMemberModeratorTableLiteral).\(groupMemberProfileIdColumnLiteral) = \(interaction[.authorId]) AND \(SQL("\(groupMemberModeratorTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.moderator)")) ) LEFT JOIN \(GroupMember.self) AS \(groupMemberAdminTableLiteral) ON ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND + \(groupMemberAdminTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(interaction[.threadId]) AND \(groupMemberAdminTableLiteral).\(groupMemberProfileIdColumnLiteral) = \(interaction[.authorId]) AND \(SQL("\(groupMemberAdminTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.admin)")) ) @@ -778,9 +822,11 @@ public extension MessageViewModel.AttachmentInteractionInfo { updatedPagedDataCache = updatedPagedDataCache.upserting( dataToUpdate.with( - attachments: attachments - .sorted() - .map { $0.attachment } + attachments: .update( + attachments + .sorted() + .map { $0.attachment } + ) ) ) } @@ -790,6 +836,92 @@ public extension MessageViewModel.AttachmentInteractionInfo { } } +// MARK: --ReactionInfo + +public extension MessageViewModel.ReactionInfo { + static let baseQuery: ((SQL?) -> AdaptedFetchRequest>) = { + return { additionalFilters -> AdaptedFetchRequest> in + let reaction: TypedTableAlias = TypedTableAlias() + let profile: TypedTableAlias = TypedTableAlias() + + let finalFilterSQL: SQL = { + guard let additionalFilters: SQL = additionalFilters else { + return SQL(stringLiteral: "") + } + + return """ + WHERE \(additionalFilters) + """ + }() + let numColumnsBeforeLinkedRecords: Int = 1 + let request: SQLRequest = """ + SELECT + \(reaction.alias[Column.rowID]) AS \(ReactionInfo.rowIdKey), + \(ReactionInfo.reactionKey).*, + \(ReactionInfo.profileKey).* + FROM \(Reaction.self) + LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(reaction[.authorId]) + \(finalFilterSQL) + """ + + return request.adapted { db in + let adapters = try splittingRowAdapters(columnCounts: [ + numColumnsBeforeLinkedRecords, + Reaction.numberOfSelectedColumns(db), + Profile.numberOfSelectedColumns(db) + ]) + + return ScopeAdapter([ + ReactionInfo.reactionString: adapters[1], + ReactionInfo.profileString: adapters[2] + ]) + } + } + }() + + static var joinToViewModelQuerySQL: SQL = { + let interaction: TypedTableAlias = TypedTableAlias() + let reaction: TypedTableAlias = TypedTableAlias() + + return """ + JOIN \(Reaction.self) ON \(reaction[.interactionId]) = \(interaction[.id]) + """ + }() + + static func createAssociateDataClosure() -> (DataCache, DataCache) -> DataCache { + return { dataCache, pagedDataCache -> DataCache in + var updatedPagedDataCache: DataCache = pagedDataCache + var pagedRowIdsWithNoReactions: Set = Set(pagedDataCache.data.keys) + + // Add any new reactions + dataCache + .values + .grouped(by: \.reaction.interactionId) + .forEach { (interactionId: Int64, reactionInfo: [MessageViewModel.ReactionInfo]) in + guard + let interactionRowId: Int64 = updatedPagedDataCache.lookup[interactionId], + let dataToUpdate: ViewModel = updatedPagedDataCache.data[interactionRowId] + else { return } + + updatedPagedDataCache = updatedPagedDataCache.upserting( + dataToUpdate.with(reactionInfo: .update(reactionInfo.sorted())) + ) + pagedRowIdsWithNoReactions.remove(interactionRowId) + } + + // Remove any removed reactions + updatedPagedDataCache = updatedPagedDataCache.upserting( + items: pagedRowIdsWithNoReactions + .compactMap { rowId -> ViewModel? in updatedPagedDataCache.data[rowId] } + .filter { viewModel -> Bool in (viewModel.reactionInfo?.isEmpty == false) } + .map { viewModel -> ViewModel in viewModel.with(reactionInfo: nil) } + ) + + return updatedPagedDataCache + } + } +} + // MARK: --TypingIndicatorInfo public extension MessageViewModel.TypingIndicatorInfo { diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 58e07f7e5..3d2ca2867 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -126,6 +126,7 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat private let authorNameInternal: String? public let currentUserPublicKey: String public let currentUserBlindedPublicKey: String? + public let recentReactionEmoji: [String]? // UI specific logic @@ -278,12 +279,64 @@ public extension SessionThreadViewModel { self.authorNameInternal = nil self.currentUserPublicKey = getUserHexEncodedPublicKey() self.currentUserBlindedPublicKey = nil + self.recentReactionEmoji = nil } } // MARK: - Mutation public extension SessionThreadViewModel { + func with( + recentReactionEmoji: [String]? = nil + ) -> SessionThreadViewModel { + return SessionThreadViewModel( + rowId: self.rowId, + threadId: self.threadId, + threadVariant: self.threadVariant, + threadCreationDateTimestamp: self.threadCreationDateTimestamp, + threadMemberNames: self.threadMemberNames, + threadIsNoteToSelf: self.threadIsNoteToSelf, + threadIsMessageRequest: self.threadIsMessageRequest, + threadRequiresApproval: self.threadRequiresApproval, + threadShouldBeVisible: self.threadShouldBeVisible, + threadIsPinned: self.threadIsPinned, + threadIsBlocked: self.threadIsBlocked, + threadMutedUntilTimestamp: self.threadMutedUntilTimestamp, + threadOnlyNotifyForMentions: self.threadOnlyNotifyForMentions, + threadMessageDraft: self.threadMessageDraft, + threadContactIsTyping: self.threadContactIsTyping, + threadUnreadCount: self.threadUnreadCount, + threadUnreadMentionCount: self.threadUnreadMentionCount, + contactProfile: self.contactProfile, + closedGroupProfileFront: self.closedGroupProfileFront, + closedGroupProfileBack: self.closedGroupProfileBack, + closedGroupProfileBackFallback: self.closedGroupProfileBackFallback, + closedGroupName: self.closedGroupName, + closedGroupUserCount: self.closedGroupUserCount, + currentUserIsClosedGroupMember: self.currentUserIsClosedGroupMember, + currentUserIsClosedGroupAdmin: self.currentUserIsClosedGroupAdmin, + openGroupName: self.openGroupName, + openGroupServer: self.openGroupServer, + openGroupRoomToken: self.openGroupRoomToken, + openGroupProfilePictureData: self.openGroupProfilePictureData, + openGroupUserCount: self.openGroupUserCount, + interactionId: self.interactionId, + interactionVariant: self.interactionVariant, + interactionTimestampMs: self.interactionTimestampMs, + interactionBody: self.interactionBody, + interactionState: self.interactionState, + interactionIsOpenGroupInvitation: self.interactionIsOpenGroupInvitation, + interactionAttachmentDescriptionInfo: self.interactionAttachmentDescriptionInfo, + interactionAttachmentCount: self.interactionAttachmentCount, + authorId: self.authorId, + threadContactNameInternal: self.threadContactNameInternal, + authorNameInternal: self.authorNameInternal, + currentUserPublicKey: self.currentUserPublicKey, + currentUserBlindedPublicKey: self.currentUserBlindedPublicKey, + recentReactionEmoji: (recentReactionEmoji ?? self.recentReactionEmoji) + ) + } + func populatingCurrentUserBlindedKey( currentUserBlindedPublicKeyForThisThread: String? = nil ) -> SessionThreadViewModel { @@ -336,7 +389,8 @@ public extension SessionThreadViewModel { threadId: self.threadId, threadVariant: self.threadVariant ) - ) + ), + recentReactionEmoji: self.recentReactionEmoji ) } } @@ -350,7 +404,6 @@ public extension SessionThreadViewModel { /// but including this warning just in case there is a discrepancy) static func baseQuery( userPublicKey: String, - filterSQL: SQL, groupSQL: SQL, orderSQL: SQL ) -> (([Int64]) -> AdaptedFetchRequest>) { @@ -368,6 +421,7 @@ public extension SessionThreadViewModel { let interactionAttachment: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() + let interactionTimestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) let profileNicknameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.nickname.name) let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name) @@ -412,7 +466,7 @@ public extension SessionThreadViewModel { \(Interaction.self).\(ViewModel.interactionIdKey), \(Interaction.self).\(ViewModel.interactionVariantKey), - \(Interaction.self).\(ViewModel.interactionTimestampMsKey), + \(Interaction.self).\(interactionTimestampMsColumnLiteral) AS \(ViewModel.interactionTimestampMsKey), \(Interaction.self).\(ViewModel.interactionBodyKey), -- Default to 'sending' assuming non-processed interaction when null @@ -440,7 +494,7 @@ public extension SessionThreadViewModel { \(interaction[.id]) AS \(ViewModel.interactionIdKey), \(interaction[.threadId]), \(interaction[.variant]) AS \(ViewModel.interactionVariantKey), - MAX(\(interaction[.timestampMs])) AS \(ViewModel.interactionTimestampMsKey), + MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral), \(interaction[.body]) AS \(ViewModel.interactionBodyKey), \(interaction[.authorId]), \(interaction[.linkPreviewUrl]), @@ -461,7 +515,7 @@ public extension SessionThreadViewModel { LEFT JOIN \(LinkPreview.self) ON ( \(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND \(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)")) AND - \(Interaction.linkPreviewFilterLiteral(timestampColumn: ViewModel.interactionTimestampMsKey)) + \(Interaction.linkPreviewFilterLiteral(timestampColumn: interactionTimestampMsColumnLiteral)) ) LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON ( \(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 AND @@ -545,12 +599,14 @@ public extension SessionThreadViewModel { let contact: TypedTableAlias = TypedTableAlias() let interaction: TypedTableAlias = TypedTableAlias() + let interactionTimestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) + return """ LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN ( SELECT \(interaction[.threadId]), - MAX(\(interaction[.timestampMs])) AS \(ViewModel.interactionTimestampMsKey) + MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral) FROM \(Interaction.self) WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) GROUP BY \(interaction[.threadId]) @@ -561,6 +617,7 @@ public extension SessionThreadViewModel { static func homeFilterSQL(userPublicKey: String) -> SQL { let thread: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() + let interaction: TypedTableAlias = TypedTableAlias() return """ \(thread[.shouldBeVisible]) = true AND ( @@ -571,7 +628,7 @@ public extension SessionThreadViewModel { ) AND ( -- Only show the 'Note to Self' thread if it has an interaction \(SQL("\(thread[.id]) != \(userPublicKey)")) OR - \(Interaction.self).\(ViewModel.interactionTimestampMsKey) IS NOT NULL + \(interaction[.timestampMs]) IS NOT NULL ) """ } @@ -598,14 +655,16 @@ public extension SessionThreadViewModel { static let homeOrderSQL: SQL = { let thread: TypedTableAlias = TypedTableAlias() + let interaction: TypedTableAlias = TypedTableAlias() - return SQL("\(thread[.isPinned]) DESC, IFNULL(\(Interaction.self).\(ViewModel.interactionTimestampMsKey), (\(thread[.creationDateTimestamp]) * 1000)) DESC") + return SQL("\(thread[.isPinned]) DESC, IFNULL(\(interaction[.timestampMs]), (\(thread[.creationDateTimestamp]) * 1000)) DESC") }() static let messageRequetsOrderSQL: SQL = { let thread: TypedTableAlias = TypedTableAlias() + let interaction: TypedTableAlias = TypedTableAlias() - return SQL("IFNULL(\(Interaction.self).\(ViewModel.interactionTimestampMsKey), (\(thread[.creationDateTimestamp]) * 1000)) DESC") + return SQL("IFNULL(\(interaction[.timestampMs]), (\(thread[.creationDateTimestamp]) * 1000)) DESC") }() } @@ -684,7 +743,7 @@ public extension SessionThreadViewModel { SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey) FROM \(Interaction.self) - GROUP BY \(interaction[.threadId]) + WHERE \(SQL("\(interaction[.threadId]) = \(threadId)")) ) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id]) LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) @@ -698,17 +757,15 @@ public extension SessionThreadViewModel { LEFT JOIN ( SELECT \(groupMember[.groupId]), - COUNT(*) AS \(ViewModel.closedGroupUserCountKey) + COUNT(\(groupMember.alias[Column.rowID])) AS \(ViewModel.closedGroupUserCountKey) FROM \(GroupMember.self) WHERE ( \(SQL("\(groupMember[.groupId]) = \(threadId)")) AND \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) ) - GROUP BY \(groupMember[.groupId]) ) AS \(closedGroupUserCountTableLiteral) ON \(SQL("\(closedGroupUserCountTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(threadId)")) WHERE \(SQL("\(thread[.id]) = \(threadId)")) - GROUP BY \(thread[.id]) """ return request.adapted { db in diff --git a/SessionMessagingKit/Utilities/Preferences.swift b/SessionMessagingKit/Utilities/Preferences.swift index ca164888e..1b406c118 100644 --- a/SessionMessagingKit/Utilities/Preferences.swift +++ b/SessionMessagingKit/Utilities/Preferences.swift @@ -69,6 +69,14 @@ public extension Setting.StringKey { /// This is the most recently recorded Voip token static let lastRecordedVoipToken: Setting.StringKey = "lastRecordedVoipToken" + + /// This is the last six emoji used by the user + static let recentReactionEmoji: Setting.StringKey = "recentReactionEmoji" + + /// This is the preferred skin tones preference for the given emoji + static func emojiPreferredSkinTones(emoji: String) -> Setting.StringKey { + return Setting.StringKey("preferredSkinTones-\(emoji)") + } } public extension Setting.DoubleKey { diff --git a/SessionMessagingKit/Utilities/SMKDependencies.swift b/SessionMessagingKit/Utilities/SMKDependencies.swift index f7b8f4498..d4f32efae 100644 --- a/SessionMessagingKit/Utilities/SMKDependencies.swift +++ b/SessionMessagingKit/Utilities/SMKDependencies.swift @@ -6,58 +6,58 @@ import SessionSnodeKit import SessionUtilitiesKit public class SMKDependencies: Dependencies { - internal var _onionApi: OnionRequestAPIType.Type? + internal var _onionApi: Atomic public var onionApi: OnionRequestAPIType.Type { get { Dependencies.getValueSettingIfNull(&_onionApi) { OnionRequestAPI.self } } - set { _onionApi = newValue } + set { _onionApi.mutate { $0 = newValue } } } - internal var _sodium: SodiumType? + internal var _sodium: Atomic public var sodium: SodiumType { get { Dependencies.getValueSettingIfNull(&_sodium) { Sodium() } } - set { _sodium = newValue } + set { _sodium.mutate { $0 = newValue } } } - internal var _box: BoxType? + internal var _box: Atomic public var box: BoxType { get { Dependencies.getValueSettingIfNull(&_box) { sodium.getBox() } } - set { _box = newValue } + set { _box.mutate { $0 = newValue } } } - internal var _genericHash: GenericHashType? + internal var _genericHash: Atomic public var genericHash: GenericHashType { get { Dependencies.getValueSettingIfNull(&_genericHash) { sodium.getGenericHash() } } - set { _genericHash = newValue } + set { _genericHash.mutate { $0 = newValue } } } - internal var _sign: SignType? + internal var _sign: Atomic public var sign: SignType { get { Dependencies.getValueSettingIfNull(&_sign) { sodium.getSign() } } - set { _sign = newValue } + set { _sign.mutate { $0 = newValue } } } - internal var _aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? + internal var _aeadXChaCha20Poly1305Ietf: Atomic public var aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType { get { Dependencies.getValueSettingIfNull(&_aeadXChaCha20Poly1305Ietf) { sodium.getAeadXChaCha20Poly1305Ietf() } } - set { _aeadXChaCha20Poly1305Ietf = newValue } + set { _aeadXChaCha20Poly1305Ietf.mutate { $0 = newValue } } } - internal var _ed25519: Ed25519Type? + internal var _ed25519: Atomic public var ed25519: Ed25519Type { get { Dependencies.getValueSettingIfNull(&_ed25519) { Ed25519Wrapper() } } - set { _ed25519 = newValue } + set { _ed25519.mutate { $0 = newValue } } } - internal var _nonceGenerator16: NonceGenerator16ByteType? + internal var _nonceGenerator16: Atomic public var nonceGenerator16: NonceGenerator16ByteType { get { Dependencies.getValueSettingIfNull(&_nonceGenerator16) { OpenGroupAPI.NonceGenerator16Byte() } } - set { _nonceGenerator16 = newValue } + set { _nonceGenerator16.mutate { $0 = newValue } } } - internal var _nonceGenerator24: NonceGenerator24ByteType? + internal var _nonceGenerator24: Atomic public var nonceGenerator24: NonceGenerator24ByteType { get { Dependencies.getValueSettingIfNull(&_nonceGenerator24) { OpenGroupAPI.NonceGenerator24Byte() } } - set { _nonceGenerator24 = newValue } + set { _nonceGenerator24.mutate { $0 = newValue } } } // MARK: - Initialization @@ -77,15 +77,15 @@ public class SMKDependencies: Dependencies { standardUserDefaults: UserDefaultsType? = nil, date: Date? = nil ) { - _onionApi = onionApi - _sodium = sodium - _box = box - _genericHash = genericHash - _sign = sign - _aeadXChaCha20Poly1305Ietf = aeadXChaCha20Poly1305Ietf - _ed25519 = ed25519 - _nonceGenerator16 = nonceGenerator16 - _nonceGenerator24 = nonceGenerator24 + _onionApi = Atomic(onionApi) + _sodium = Atomic(sodium) + _box = Atomic(box) + _genericHash = Atomic(genericHash) + _sign = Atomic(sign) + _aeadXChaCha20Poly1305Ietf = Atomic(aeadXChaCha20Poly1305Ietf) + _ed25519 = Atomic(ed25519) + _nonceGenerator16 = Atomic(nonceGenerator16) + _nonceGenerator24 = Atomic(nonceGenerator24) super.init( generalCache: generalCache, diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index e4edc8803..3af36df80 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -1243,7 +1243,8 @@ class OpenGroupAPISpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: nil, - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) override class var mockResponse: Data? { return try! JSONEncoder().encode(data) } @@ -1612,7 +1613,8 @@ class OpenGroupAPISpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: nil, - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) override class var mockResponse: Data? { return try! JSONEncoder().encode(data) } diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 9a598a15b..d581ac276 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -204,7 +204,8 @@ class OpenGroupManagerSpec: QuickSpec { "AAAAAAAAAAAAAAAAAAAAA", "AA" ].joined(), - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) testDirectMessage = OpenGroupAPI.DirectMessage( id: 128, @@ -229,6 +230,7 @@ class OpenGroupManagerSpec: QuickSpec { try testOpenGroup.insert(db) try Capability(openGroupServer: testOpenGroup.server, variant: .sogs, isMissing: false).insert(db) } + mockOGMCache.when { $0.pendingChanges }.thenReturn([]) mockGeneralCache.when { $0.encodedPublicKey }.thenReturn("05\(TestConstants.publicKey)") mockGenericHash.when { $0.hash(message: anyArray(), outputLength: any()) }.thenReturn([]) mockSodium @@ -2115,12 +2117,12 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: nil, - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) ], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2142,7 +2144,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2177,12 +2178,12 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: Data([1, 2, 3]).base64EncodedString(), - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) ], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2210,12 +2211,12 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: Data([1, 2, 3]).base64EncodedString(), - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) ], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2230,7 +2231,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testMessage], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2254,13 +2254,13 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: Data([1, 2, 3]).base64EncodedString(), - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ), testMessage, ], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2293,12 +2293,12 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: nil, - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) ], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2322,12 +2322,12 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: nil, - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) ], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2379,7 +2379,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [], fromOutbox: false, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2413,7 +2412,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: false, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2452,7 +2450,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: false, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2478,7 +2475,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: false, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2509,7 +2505,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: false, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2524,7 +2519,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: false, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2549,7 +2543,6 @@ class OpenGroupManagerSpec: QuickSpec { ], fromOutbox: false, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2576,7 +2569,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: true, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2607,7 +2599,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: true, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2623,7 +2614,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: true, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2659,7 +2649,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: true, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2674,7 +2663,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: true, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2699,7 +2687,6 @@ class OpenGroupManagerSpec: QuickSpec { ], fromOutbox: true, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } diff --git a/SessionMessagingKitTests/_TestUtilities/DependencyExtensions.swift b/SessionMessagingKitTests/_TestUtilities/DependencyExtensions.swift index 5c2f8de5d..51bb86598 100644 --- a/SessionMessagingKitTests/_TestUtilities/DependencyExtensions.swift +++ b/SessionMessagingKitTests/_TestUtilities/DependencyExtensions.swift @@ -23,19 +23,19 @@ extension SMKDependencies { date: Date? = nil ) -> SMKDependencies { return SMKDependencies( - onionApi: (onionApi ?? self._onionApi), - generalCache: (generalCache ?? self._generalCache), - storage: (storage ?? self._storage), - sodium: (sodium ?? self._sodium), - box: (box ?? self._box), - genericHash: (genericHash ?? self._genericHash), - sign: (sign ?? self._sign), - aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf), - ed25519: (ed25519 ?? self._ed25519), - nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16), - nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24), - standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults), - date: (date ?? self._date) + onionApi: (onionApi ?? self._onionApi.wrappedValue), + generalCache: (generalCache ?? self._generalCache.wrappedValue), + storage: (storage ?? self._storage.wrappedValue), + sodium: (sodium ?? self._sodium.wrappedValue), + box: (box ?? self._box.wrappedValue), + genericHash: (genericHash ?? self._genericHash.wrappedValue), + sign: (sign ?? self._sign.wrappedValue), + aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf.wrappedValue), + ed25519: (ed25519 ?? self._ed25519.wrappedValue), + nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16.wrappedValue), + nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24.wrappedValue), + standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults.wrappedValue), + date: (date ?? self._date.wrappedValue) ) } } diff --git a/SessionMessagingKitTests/_TestUtilities/MockGeneralCache.swift b/SessionMessagingKitTests/_TestUtilities/MockGeneralCache.swift index c47b8d6eb..bf687f6d0 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockGeneralCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockGeneralCache.swift @@ -10,4 +10,9 @@ class MockGeneralCache: Mock, GeneralCacheType { get { return accept() as? String } set { accept(args: [newValue]) } } + + var recentReactionTimestamps: [Int64] { + get { return accept() as! [Int64] } + set { accept(args: [newValue]) } + } } diff --git a/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift index 31bace48f..02caa5e85 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift @@ -37,6 +37,11 @@ class MockOGMCache: Mock, OGMCacheType { set { accept(args: [newValue]) } } + var pendingChanges: [OpenGroupAPI.PendingChange] { + get { return accept() as! [OpenGroupAPI.PendingChange] } + set { accept(args: [newValue]) } + } + func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval { return accept(args: [dependencies]) as! TimeInterval } diff --git a/SessionMessagingKitTests/_TestUtilities/OGMDependencyExtensions.swift b/SessionMessagingKitTests/_TestUtilities/OGMDependencyExtensions.swift index d559bdfec..0d2f8cee9 100644 --- a/SessionMessagingKitTests/_TestUtilities/OGMDependencyExtensions.swift +++ b/SessionMessagingKitTests/_TestUtilities/OGMDependencyExtensions.swift @@ -24,20 +24,20 @@ extension OpenGroupManager.OGMDependencies { date: Date? = nil ) -> OpenGroupManager.OGMDependencies { return OpenGroupManager.OGMDependencies( - cache: (cache ?? self._mutableCache), - onionApi: (onionApi ?? self._onionApi), - generalCache: (generalCache ?? self._generalCache), - storage: (storage ?? self._storage), - sodium: (sodium ?? self._sodium), - box: (box ?? self._box), - genericHash: (genericHash ?? self._genericHash), - sign: (sign ?? self._sign), - aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf), - ed25519: (ed25519 ?? self._ed25519), - nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16), - nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24), - standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults), - date: (date ?? self._date) + cache: (cache ?? self._mutableCache.wrappedValue), + onionApi: (onionApi ?? self._onionApi.wrappedValue), + generalCache: (generalCache ?? self._generalCache.wrappedValue), + storage: (storage ?? self._storage.wrappedValue), + sodium: (sodium ?? self._sodium.wrappedValue), + box: (box ?? self._box.wrappedValue), + genericHash: (genericHash ?? self._genericHash.wrappedValue), + sign: (sign ?? self._sign.wrappedValue), + aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf.wrappedValue), + ed25519: (ed25519 ?? self._ed25519.wrappedValue), + nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16.wrappedValue), + nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24.wrappedValue), + standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults.wrappedValue), + date: (date ?? self._date.wrappedValue) ) } } diff --git a/SessionNotificationServiceExtension/NSENotificationPresenter.swift b/SessionNotificationServiceExtension/NSENotificationPresenter.swift index 04503ed5f..62956ff80 100644 --- a/SessionNotificationServiceExtension/NSENotificationPresenter.swift +++ b/SessionNotificationServiceExtension/NSENotificationPresenter.swift @@ -9,7 +9,7 @@ import SessionMessagingKit public class NSENotificationPresenter: NSObject, NotificationsProtocol { private var notifications: [String: UNNotificationRequest] = [:] - public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, isBackgroundPoll: Bool) { + public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread) { let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true) // Ensure we should be showing a notification for the thread @@ -18,6 +18,12 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { } let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant) + let groupName: String = SessionThread.displayName( + threadId: thread.id, + variant: thread.variant, + closedGroupName: (try? thread.closedGroup.fetchOne(db))?.name, + openGroupName: (try? thread.openGroup.fetchOne(db))?.name + ) var notificationTitle: String = senderName if thread.variant == .closedGroup || thread.variant == .openGroup { @@ -26,22 +32,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { return } - notificationTitle = { - let groupName: String = SessionThread.displayName( - threadId: thread.id, - variant: thread.variant, - closedGroupName: (try? thread.closedGroup.fetchOne(db))?.name, - openGroupName: (try? thread.openGroup.fetchOne(db))?.name - ) - - guard !isBackgroundPoll else { return groupName } - - return String( - format: NotificationStrings.incomingGroupMessageTitleFormat, - senderName, - groupName - ) - }() + notificationTitle = String( + format: NotificationStrings.incomingGroupMessageTitleFormat, + senderName, + groupName + ) } let snippet: String = (interaction.previewText(db) @@ -88,21 +83,31 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { notificationContent.body = "MESSAGE_REQUESTS_NOTIFICATION".localized() } - // Add request - let identifier = interaction.notificationIdentifier(isBackgroundPoll: isBackgroundPoll) + // Add request (try to group notifications for interactions from open groups) + let identifier: String = interaction.notificationIdentifier( + shouldGroupMessagesForThread: (thread.variant == .openGroup) + ) var trigger: UNNotificationTrigger? - if isBackgroundPoll { - trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false) + if thread.variant == .openGroup { + trigger = UNTimeIntervalNotificationTrigger( + timeInterval: Notifications.delayForGroupedNotifications, + repeats: false + ) - var numberOfNotifications: Int = (notifications[identifier]? + let numberExistingNotifications: Int? = notifications[identifier]? .content .userInfo[NotificationServiceExtension.threadNotificationCounter] - .asType(Int.self)) - .defaulting(to: 1) + .asType(Int.self) + var numberOfNotifications: Int = (numberExistingNotifications ?? 1) - if numberOfNotifications > 1 { + if numberExistingNotifications != nil { numberOfNotifications += 1 // Add one for the current notification + + notificationContent.title = (previewType == .noNameNoPreview ? + notificationContent.title : + groupName + ) notificationContent.body = String( format: NotificationStrings.incomingCollapsedMessagesBody, "\(numberOfNotifications)" @@ -112,20 +117,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { notificationContent.userInfo[NotificationServiceExtension.threadNotificationCounter] = numberOfNotifications } - let request = UNNotificationRequest(identifier: identifier, content: notificationContent, trigger: trigger) - - SNLog("Add remote notification request: \(notificationContent.body)") - let semaphore = DispatchSemaphore(value: 0) - UNUserNotificationCenter.current().add(request) { error in - if let error = error { - SNLog("Failed to add notification request due to error:\(error)") - } - - self.notifications[identifier] = request - semaphore.signal() - } - semaphore.wait() - SNLog("Finish adding remote notification request") + addNotifcationRequest( + identifier: identifier, + notificationContent: notificationContent, + trigger: trigger + ) } public func notifyUser(_ db: Database, forIncomingCall interaction: Interaction, in thread: SessionThread) { @@ -176,20 +172,46 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { ) } - // Add request - let identifier = UUID().uuidString - let request = UNNotificationRequest(identifier: identifier, content: notificationContent, trigger: nil) + addNotifcationRequest( + identifier: UUID().uuidString, + notificationContent: notificationContent, + trigger: nil + ) + } + + public func notifyUser(_ db: Database, forReaction reaction: Reaction, in thread: SessionThread) { + let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true) - SNLog("Add remote notification request: \(notificationContent.body)") - let semaphore = DispatchSemaphore(value: 0) - UNUserNotificationCenter.current().add(request) { error in - if let error = error { - SNLog("Failed to add notification request due to error:\(error)") - } - semaphore.signal() + // No reaction notifications for muted, group threads or message requests + guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return } + guard thread.variant != .closedGroup && thread.variant != .openGroup else { return } + guard !isMessageRequest else { return } + + let senderName: String = Profile.displayName(db, id: reaction.authorId, threadVariant: thread.variant) + let notificationTitle = "Session" + var notificationBody = String(format: "EMOJI_REACTS_NOTIFICATION".localized(), senderName, reaction.emoji) + + // Title & body + let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType] + .defaulting(to: .nameAndPreview) + + switch previewType { + case .nameAndPreview: break + default: notificationBody = NotificationStrings.incomingMessageBody } - semaphore.wait() - SNLog("Finish adding remote notification request") + + var userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ] + userInfo[NotificationServiceExtension.threadIdKey] = thread.id + + let notificationContent = UNMutableNotificationContent() + notificationContent.userInfo = userInfo + notificationContent.sound = thread.notificationSound + .defaulting(to: db[.defaultNotificationSound] ?? Preferences.Sound.defaultNotificationSound) + .notificationSound(isQuiet: false) + notificationContent.title = notificationTitle + notificationContent.body = notificationBody + + addNotifcationRequest(identifier: UUID().uuidString, notificationContent: notificationContent, trigger: nil) } public func cancelNotifications(identifiers: [String]) { @@ -203,6 +225,21 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { notificationCenter.removeAllPendingNotificationRequests() notificationCenter.removeAllDeliveredNotifications() } + + private func addNotifcationRequest(identifier: String, notificationContent: UNNotificationContent, trigger: UNNotificationTrigger?) { + let request = UNNotificationRequest(identifier: identifier, content: notificationContent, trigger: trigger) + + SNLog("Add remote notification request: \(notificationContent.body)") + let semaphore = DispatchSemaphore(value: 0) + UNUserNotificationCenter.current().add(request) { error in + if let error = error { + SNLog("Failed to add notification request due to error:\(error)") + } + semaphore.signal() + } + semaphore.wait() + SNLog("Finish adding remote notification request") + } } private extension String { diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index df3fed80c..5705a4661 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -83,8 +83,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension db, message: visibleMessage, associatedWithProto: processedMessage.proto, - openGroupId: (isOpenGroup ? processedMessage.threadId : nil), - isBackgroundPoll: false + openGroupId: (isOpenGroup ? processedMessage.threadId : nil) ) // Remove the notifications if there is an outgoing messages from a linked device @@ -329,7 +328,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension .defaulting(to: []) .map { server in OpenGroupAPI.Poller(for: server) - .poll(isBackgroundPoll: true, isPostCapabilitiesRetry: false) + .poll(calledFromBackgroundPoller: true, isPostCapabilitiesRetry: false) .timeout( seconds: 20, timeoutError: NotificationServiceError.timeout diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index 3efd5d4ba..222027bd3 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -588,7 +588,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { case .v4: // Note: We need to remove the leading forward slash unless we are explicitly hitting a legacy // endpoint (in which case we need it to ensure the request signing works correctly - let endpoint: String = url.path + let endpoint: String = url.path .appending(url.query.map { value in "?\(value)" }) let requestInfo: RequestInfo = RequestInfo( diff --git a/SessionSnodeKit/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI.swift index 2e5b5fc1f..1106f56c4 100644 --- a/SessionSnodeKit/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI.swift @@ -24,7 +24,7 @@ public final class SnodeAPI { /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. public static var clockOffset: Int64 = 0 /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. - public static var swarmCache: [String: Set] = [:] + public static var swarmCache: Atomic<[String: Set]> = Atomic([:]) // MARK: - Namespaces @@ -96,10 +96,11 @@ public final class SnodeAPI { private static func loadSwarmIfNeeded(for publicKey: String) { guard !loadedSwarms.contains(publicKey) else { return } - Storage.shared.read { db in - swarmCache[publicKey] = ((try? Snode.fetchSet(db, publicKey: publicKey)) ?? []) - } + let updatedCacheForKey: Set = Storage.shared + .read { db in try Snode.fetchSet(db, publicKey: publicKey) } + .defaulting(to: []) + swarmCache.mutate { $0[publicKey] = updatedCacheForKey } loadedSwarms.insert(publicKey) } @@ -107,7 +108,8 @@ public final class SnodeAPI { #if DEBUG dispatchPrecondition(condition: .onQueue(Threading.workQueue)) #endif - swarmCache[publicKey] = newValue + swarmCache.mutate { $0[publicKey] = newValue } + guard persist else { return } Storage.shared.write { db in @@ -119,7 +121,7 @@ public final class SnodeAPI { #if DEBUG dispatchPrecondition(condition: .onQueue(Threading.workQueue)) #endif - let swarmOrNil = swarmCache[publicKey] + let swarmOrNil = swarmCache.wrappedValue[publicKey] guard var swarm = swarmOrNil, let index = swarm.firstIndex(of: snode) else { return } swarm.remove(at: index) setSwarm(to: swarm, for: publicKey) @@ -460,7 +462,7 @@ public final class SnodeAPI { public static func getSwarm(for publicKey: String) -> Promise> { loadSwarmIfNeeded(for: publicKey) - if let cachedSwarm = swarmCache[publicKey], cachedSwarm.count >= minSwarmSnodeCount { + if let cachedSwarm = swarmCache.wrappedValue[publicKey], cachedSwarm.count >= minSwarmSnodeCount { return Promise> { $0.fulfill(cachedSwarm) } } diff --git a/Session/Shared/HighlightMentionBackgroundView.swift b/SessionUIKit/Components/HighlightMentionBackgroundView.swift similarity index 80% rename from Session/Shared/HighlightMentionBackgroundView.swift rename to SessionUIKit/Components/HighlightMentionBackgroundView.swift index 3a7db4eaa..450636c53 100644 --- a/Session/Shared/HighlightMentionBackgroundView.swift +++ b/SessionUIKit/Components/HighlightMentionBackgroundView.swift @@ -8,14 +8,16 @@ public extension NSAttributedString.Key { static let currentUserMentionBackgroundPadding: NSAttributedString.Key = NSAttributedString.Key(rawValue: "currentUserMentionBackgroundPadding") } -class HighlightMentionBackgroundView: UIView { +public class HighlightMentionBackgroundView: UIView { + weak var targetLabel: UILabel? var maxPadding: CGFloat = 0 - init() { + init(targetLabel: UILabel) { + self.targetLabel = targetLabel + super.init(frame: .zero) self.isOpaque = false - self.layer.zPosition = -1 } required init?(coder: NSCoder) { @@ -50,17 +52,19 @@ class HighlightMentionBackgroundView: UIView { } } - return allMentionRadii + let maxRadii: CGFloat? = allMentionRadii .compactMap { $0 } .max() - .defaulting(to: 0) + + return (maxRadii ?? 0) } // MARK: - Drawing - override func draw(_ rect: CGRect) { + override public func draw(_ rect: CGRect) { guard - let superview: UITextView = (self.superview as? UITextView), + let targetLabel: UILabel = self.targetLabel, + let attributedText: NSAttributedString = targetLabel.attributedText, let context = UIGraphicsGetCurrentContext() else { return } @@ -69,14 +73,14 @@ class HighlightMentionBackgroundView: UIView { context.translateBy(x: 0, y: bounds.size.height) context.scaleBy(x: 1.0, y: -1.0) - // Note: Calculations MUST happen based on the 'superview' size as this class has extra padding which - // can result in calculations being off + // Note: Calculations MUST happen based on the 'targetLabel' size as this class has extra padding + // which can result in calculations being off let path = CGMutablePath() - let size = superview.sizeThatFits(CGSize(width: superview.bounds.width, height: .greatestFiniteMagnitude)) + let size = targetLabel.sizeThatFits(CGSize(width: targetLabel.bounds.width, height: .greatestFiniteMagnitude)) path.addRect(CGRect(x: 0, y: 0, width: size.width, height: size.height), transform: .identity) - let framesetter = CTFramesetterCreateWithAttributedString(superview.attributedText as CFAttributedString) - let frame: CTFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, superview.attributedText.length), path, nil) + let framesetter = CTFramesetterCreateWithAttributedString(attributedText as CFAttributedString) + let frame: CTFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributedText.length), path, nil) let lines: [CTLine] = frame.lines var origins = [CGPoint](repeating: .zero, count: lines.count) @@ -97,12 +101,11 @@ class HighlightMentionBackgroundView: UIView { continue } - let cornerRadius: CGFloat = (attributes + let maybeCornerRadius: CGFloat? = (attributes .value(forKey: NSAttributedString.Key.currentUserMentionBackgroundCornerRadius.rawValue) as? CGFloat) - .defaulting(to: 0) - let padding: CGFloat = (attributes + let maybePadding: CGFloat? = (attributes .value(forKey: NSAttributedString.Key.currentUserMentionBackgroundPadding.rawValue) as? CGFloat) - .defaulting(to: 0) + let padding: CGFloat = (maybePadding ?? 0) let range = CTRunGetStringRange(run) var runBounds: CGRect = .zero @@ -121,10 +124,10 @@ class HighlightMentionBackgroundView: UIView { } }() - // HACK: This `extraYOffset` value is a hack to resolve a weird issue where the positioning - // seems to be slightly off every additional line of text we add (it doesn't seem to be related - // to line spacing or anything, more related to the bold mention text being positioned slightly - // differently from the non-bold text) + // HACK: This `extraYOffset` value is a hack to resolve a weird issue where the + // positioning seems to be slightly off every additional line of text we add (it + // doesn't seem to be related to line spacing or anything, more related to the + // bold mention text being positioned slightly differently from the non-bold text) let extraYOffset: CGFloat = (CGFloat(lineIndex) * (runDescent / 12)) // Note: Changes to `origin.y` need to be inverted since the context has been flipped @@ -137,7 +140,7 @@ class HighlightMentionBackgroundView: UIView { extraYOffset ) - let path = UIBezierPath(roundedRect: runBounds, cornerRadius: cornerRadius) + let path = UIBezierPath(roundedRect: runBounds, cornerRadius: (maybeCornerRadius ?? 0)) mentionBackgroundColor.setFill() path.fill() } diff --git a/SessionUIKit/Components/TappableLabel.swift b/SessionUIKit/Components/TappableLabel.swift new file mode 100644 index 000000000..b982cade5 --- /dev/null +++ b/SessionUIKit/Components/TappableLabel.swift @@ -0,0 +1,132 @@ +import UIKit + +// Requirements: +// â€ĸ Links should show up properly and be tappable. +// â€ĸ Text should * not * be selectable. +// â€ĸ The long press interaction that shows the context menu should still work. + +// See https://stackoverflow.com/questions/47983838/how-can-you-change-the-color-of-links-in-a-uilabel + +public protocol TappableLabelDelegate: AnyObject { + func tapableLabel(_ label: TappableLabel, didTapUrl url: String, atRange range: NSRange) +} + +public class TappableLabel: UILabel { + private var links: [String: NSRange] = [:] + private lazy var highlightedMentionBackgroundView: HighlightMentionBackgroundView = HighlightMentionBackgroundView(targetLabel: self) + private(set) var layoutManager = NSLayoutManager() + private(set) var textContainer = NSTextContainer(size: CGSize.zero) + private(set) var textStorage = NSTextStorage() { + didSet { + textStorage.addLayoutManager(layoutManager) + } + } + + public weak var delegate: TappableLabelDelegate? + + public override var attributedText: NSAttributedString? { + didSet { + guard let attributedText: NSAttributedString = attributedText else { + textStorage = NSTextStorage() + links = [:] + return + } + + textStorage = NSTextStorage(attributedString: attributedText) + findLinksAndRange(attributeString: attributedText) + highlightedMentionBackgroundView.maxPadding = highlightedMentionBackgroundView + .calculateMaxPadding(for: attributedText) + highlightedMentionBackgroundView.frame = self.bounds.insetBy( + dx: -highlightedMentionBackgroundView.maxPadding, + dy: -highlightedMentionBackgroundView.maxPadding + ) + } + } + + public override var lineBreakMode: NSLineBreakMode { + didSet { + textContainer.lineBreakMode = lineBreakMode + } + } + + public override var numberOfLines: Int { + didSet { + textContainer.maximumNumberOfLines = numberOfLines + } + } + + // MARK: - Initialization + + public override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + private func setup() { + isUserInteractionEnabled = true + layoutManager.addTextContainer(textContainer) + textContainer.lineFragmentPadding = 0 + textContainer.lineBreakMode = lineBreakMode + textContainer.maximumNumberOfLines = numberOfLines + numberOfLines = 0 + } + + // MARK: - Layout + + public override func didMoveToSuperview() { + super.didMoveToSuperview() + + // Note: Because we want the 'highlight' content to appear behind the label we need + // to add the 'highlightedMentionBackgroundView' below it in the view hierarchy + // + // In order to try and avoid adding even more complexity to UI components which use + // this 'TappableLabel' we are going some view hierarchy manipulation and forcing + // these elements to maintain the same superview + highlightedMentionBackgroundView.removeFromSuperview() + superview?.insertSubview(highlightedMentionBackgroundView, belowSubview: self) + } + + public override func layoutSubviews() { + super.layoutSubviews() + + textContainer.size = bounds.size + highlightedMentionBackgroundView.frame = self.frame.insetBy( + dx: -highlightedMentionBackgroundView.maxPadding, + dy: -highlightedMentionBackgroundView.maxPadding + ) + } + + // MARK: - Functions + + private func findLinksAndRange(attributeString: NSAttributedString) { + links = [:] + let enumerationBlock: (Any?, NSRange, UnsafeMutablePointer) -> Void = { [weak self] value, range, isStop in + guard let strongSelf = self else { return } + if let value = value { + let stringValue = "\(value)" + strongSelf.links[stringValue] = range + } + } + attributeString.enumerateAttribute(.link, in: NSRange(0.., with event: UIEvent?) { + guard let locationOfTouch = touches.first?.location(in: self) else { + return + } + + textContainer.size = bounds.size + + let indexOfCharacter = layoutManager.glyphIndex(for: locationOfTouch, in: textContainer) + for (urlString, range) in links where NSLocationInRange(indexOfCharacter, range) { + delegate?.tapableLabel(self, didTapUrl: urlString, atRange: range) + return + } + } +} diff --git a/SessionUIKit/Style Guide/Colors.swift b/SessionUIKit/Style Guide/Colors.swift index c2b09e5f0..3059b4518 100644 --- a/SessionUIKit/Style Guide/Colors.swift +++ b/SessionUIKit/Style Guide/Colors.swift @@ -49,4 +49,5 @@ public final class Colors : NSObject { @objc public static var sessionMessageRequestsIcon: UIColor { UIColor(named: "session_message_requests_icon")! } @objc public static var sessionMessageRequestsTitle: UIColor { UIColor(named: "session_message_requests_title")! } @objc public static var sessionMessageRequestsInfoText: UIColor { UIColor(named: "session_message_requests_info_text")! } + @objc public static var sessionEmojiPlusButtonBackground: UIColor { UIColor(named: "session_emoji_plus_button_background")! } } diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_emoji_plus_button_background.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_emoji_plus_button_background.colorset/Contents.json new file mode 100644 index 000000000..c76746b18 --- /dev/null +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_emoji_plus_button_background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x23", + "green" : "0x23", + "red" : "0x23" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_search_bar_background.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_search_bar_background.colorset/Contents.json index 2e2e0b5ba..a6efcac4b 100644 --- a/SessionUIKit/Style Guide/Colors.xcassets/session_search_bar_background.colorset/Contents.json +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_search_bar_background.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "0.120", - "blue" : "147", - "green" : "142", - "red" : "142" + "blue" : "0x93", + "green" : "0x8E", + "red" : "0x8E" } }, "idiom" : "universal" diff --git a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift index bdc17323e..c99da5e3f 100644 --- a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift +++ b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift @@ -110,9 +110,39 @@ public class PagedDatabaseObserver: TransactionObserver where // changes only include table and column info at this stage guard allObservedTableNames.contains(event.tableName) else { return } + // When generating the tracked change we need to check if the change was + // a deletion to a related table (if so then once the change is performed + // there won't be a way to associated the deleted related record to the + // original so we need to retrieve the association in here) + let trackedChange: PagedData.TrackedChange = { + guard + event.tableName != pagedTableName, + event.kind == .delete, + let observedChange: PagedData.ObservedChanges = observedTableChangeTypes[event.tableName], + let joinToPagedType: SQL = observedChange.joinToPagedType + else { return PagedData.TrackedChange(event: event) } + + // Retrieve the pagedRowId for the related value that is + // getting deleted + let pagedRowIds: [Int64] = Storage.shared + .read { db in + PagedData.pagedRowIdsForRelatedRowIds( + db, + tableName: event.tableName, + pagedTableName: pagedTableName, + relatedRowIds: [event.rowID], + joinToPagedType: joinToPagedType + ) + } + .defaulting(to: []) + + return PagedData.TrackedChange(event: event, pagedRowIdsForRelatedDeletion: pagedRowIds) + }() + // The 'event' object only exists during this method so we need to copy the info // from it, otherwise it will cease to exist after this metod call finishes changesInCommit.mutate { $0.insert(PagedData.TrackedChange(event: event)) } + changesInCommit.mutate { $0.insert(trackedChange) } } // Note: We will process all updates which come through this method even if @@ -163,7 +193,7 @@ public class PagedDatabaseObserver: TransactionObserver where associatedDataInfo.forEach { hasChanges, associatedData in guard cacheHasChanges || hasChanges else { return } - finalUpdatedDataCache = associatedData.attachAssociatedData(to: finalUpdatedDataCache) + finalUpdatedDataCache = associatedData.updateAssociatedData(to: finalUpdatedDataCache) } // Update the cache, pageInfo and the change callback @@ -180,13 +210,17 @@ public class PagedDatabaseObserver: TransactionObserver where .filter { $0.tableName == pagedTableName } let relatedChanges: [String: [PagedData.TrackedChange]] = committedChanges .filter { $0.tableName != pagedTableName } + .filter { $0.kind != .delete } .reduce(into: [:]) { result, next in guard observedTableChangeTypes[next.tableName] != nil else { return } result[next.tableName] = (result[next.tableName] ?? []).appending(next) } + let relatedDeletions: [PagedData.TrackedChange] = committedChanges + .filter { $0.tableName != pagedTableName } + .filter { $0.kind == .delete } - guard !directChanges.isEmpty || !relatedChanges.isEmpty else { + guard !directChanges.isEmpty || !relatedChanges.isEmpty || !relatedDeletions.isEmpty else { updateDataAndCallbackIfNeeded(self.dataCache.wrappedValue, self.pageInfo.wrappedValue, false) return } @@ -219,7 +253,7 @@ public class PagedDatabaseObserver: TransactionObserver where let changesToQuery: [PagedData.TrackedChange] = directChanges .filter { $0.kind != .delete } - guard !changesToQuery.isEmpty || !relatedChanges.isEmpty else { + guard !changesToQuery.isEmpty || !relatedChanges.isEmpty || !relatedDeletions.isEmpty else { updateDataAndCallbackIfNeeded(updatedDataCache, updatedPageInfo, !deletionChanges.isEmpty) return } @@ -248,7 +282,7 @@ public class PagedDatabaseObserver: TransactionObserver where .asSet() }() - guard !changesToQuery.isEmpty || !pagedRowIdsForRelatedChanges.isEmpty else { + guard !changesToQuery.isEmpty || !pagedRowIdsForRelatedChanges.isEmpty || !relatedDeletions.isEmpty else { updateDataAndCallbackIfNeeded(updatedDataCache, updatedPageInfo, !deletionChanges.isEmpty) return } @@ -270,6 +304,16 @@ public class PagedDatabaseObserver: TransactionObserver where orderSQL: orderSQL, filterSQL: filterSQL ) + let relatedDeletionIndexes: [PagedData.RowIndexInfo] = PagedData.indexes( + db, + rowIds: relatedDeletions + .compactMap { $0.pagedRowIdsForRelatedDeletion } + .flatMap { $0 }, + tableName: pagedTableName, + requiredJoinSQL: joinSQL, + orderSQL: orderSQL, + filterSQL: filterSQL + ) // Determine if the indexes for the row ids should be displayed on the screen and remove any // which shouldn't - values less than 'currentCount' or if there is at least one value less than @@ -306,6 +350,7 @@ public class PagedDatabaseObserver: TransactionObserver where } let validChangeRowIds: [Int64] = determineValidChanges(for: itemIndexes) let validRelatedChangeRowIds: [Int64] = determineValidChanges(for: relatedChangeIndexes) + let validRelatedDeletionRowIds: [Int64] = determineValidChanges(for: relatedDeletionIndexes) let countBefore: Int = itemIndexes.filter { $0.rowIndex < updatedPageInfo.pageOffset }.count // Update the offset and totalCount even if the rows are outside of the current page (need to @@ -325,13 +370,13 @@ public class PagedDatabaseObserver: TransactionObserver where // If there are no valid row ids then stop here (trigger updates though since the page info // has changes) - guard !validChangeRowIds.isEmpty || !validRelatedChangeRowIds.isEmpty else { + guard !validChangeRowIds.isEmpty || !validRelatedChangeRowIds.isEmpty || !validRelatedDeletionRowIds.isEmpty else { updateDataAndCallbackIfNeeded(updatedDataCache, updatedPageInfo, true) return } // Fetch the inserted/updated rows - let targetRowIds: [Int64] = Array((validChangeRowIds + validRelatedChangeRowIds).asSet()) + let targetRowIds: [Int64] = Array((validChangeRowIds + validRelatedChangeRowIds + validRelatedDeletionRowIds).asSet()) let updatedItems: [T] = (try? dataQuery(targetRowIds) .fetchAll(db)) .defaulting(to: []) @@ -379,7 +424,8 @@ public class PagedDatabaseObserver: TransactionObserver where let orderSQL: SQL = self.orderSQL let dataQuery: ([Int64]) -> AdaptedFetchRequest> = self.dataQuery - let loadedPage: (data: [T]?, pageInfo: PagedData.PageInfo)? = Storage.shared.read { [weak self] db in + let loadedPage: (data: [T]?, pageInfo: PagedData.PageInfo, failureCallback: (() -> ())?)? = Storage.shared.read { [weak self] db in + typealias QueryInfo = (limit: Int, offset: Int, updatedCacheOffset: Int) let totalCount: Int = PagedData.totalCount( db, tableName: pagedTableName, @@ -387,7 +433,7 @@ public class PagedDatabaseObserver: TransactionObserver where filterSQL: filterSQL ) - let queryInfo: (limit: Int, offset: Int, updatedCacheOffset: Int)? = { + let (queryInfo, callback): (QueryInfo?, (() -> ())?) = { switch target { case .initialPageAround(let targetId): // If we want to focus on a specific item then we need to find it's index in @@ -404,7 +450,7 @@ public class PagedDatabaseObserver: TransactionObserver where // If we couldn't find the targetId then just load the first page guard let targetIndex: Int = maybeIndex else { - return (currentPageInfo.pageSize, 0, 0) + return ((currentPageInfo.pageSize, 0, 0), nil) } let updatedOffset: Int = { @@ -421,22 +467,28 @@ public class PagedDatabaseObserver: TransactionObserver where return (targetIndex - halfPageSize) }() - return (currentPageInfo.pageSize, updatedOffset, updatedOffset) + return ((currentPageInfo.pageSize, updatedOffset, updatedOffset), nil) case .pageBefore: let updatedOffset: Int = max(0, (currentPageInfo.pageOffset - currentPageInfo.pageSize)) return ( - currentPageInfo.pageSize, - updatedOffset, - updatedOffset + ( + currentPageInfo.pageSize, + updatedOffset, + updatedOffset + ), + nil ) case .pageAfter: return ( - currentPageInfo.pageSize, - (currentPageInfo.pageOffset + currentPageInfo.currentCount), - currentPageInfo.pageOffset + ( + currentPageInfo.pageSize, + (currentPageInfo.pageOffset + currentPageInfo.currentCount), + currentPageInfo.pageOffset + ), + nil ) case .untilInclusive(let targetId, let padding): @@ -459,16 +511,19 @@ public class PagedDatabaseObserver: TransactionObserver where targetIndex < currentPageInfo.pageOffset || targetIndex >= cacheCurrentEndIndex ) - else { return nil } + else { return (nil, nil) } // If the target is before the cached data then load before if targetIndex < currentPageInfo.pageOffset { let finalIndex: Int = max(0, (targetIndex - abs(padding))) return ( - (currentPageInfo.pageOffset - finalIndex), - finalIndex, - finalIndex + ( + (currentPageInfo.pageOffset - finalIndex), + finalIndex, + finalIndex + ), + nil ) } @@ -477,23 +532,81 @@ public class PagedDatabaseObserver: TransactionObserver where let finalIndex: Int = min(totalCount, (targetIndex + 1 + abs(padding))) return ( - (finalIndex - cacheCurrentEndIndex), - cacheCurrentEndIndex, - currentPageInfo.pageOffset + ( + (finalIndex - cacheCurrentEndIndex), + cacheCurrentEndIndex, + currentPageInfo.pageOffset + ), + nil ) + case .jumpTo(let targetId, let paddingForInclusive): + // If we want to focus on a specific item then we need to find it's index in + // the queried data + let maybeIndex: Int? = PagedData.index( + db, + for: targetId, + tableName: pagedTableName, + idColumn: idColumnName, + orderSQL: orderSQL, + filterSQL: filterSQL + ) + let cacheCurrentEndIndex: Int = (currentPageInfo.pageOffset + currentPageInfo.currentCount) + + // If we couldn't find the targetId or it's already in the cache then do nothing + guard + let targetIndex: Int = maybeIndex.map({ max(0, min(totalCount, $0)) }), + ( + targetIndex < currentPageInfo.pageOffset || + targetIndex >= cacheCurrentEndIndex + ) + else { return (nil, nil) } + + // If the target id is within a single page of the current cached data + // then trigger the `untilInclusive` behaviour instead + guard + abs(targetIndex - cacheCurrentEndIndex) > currentPageInfo.pageSize || + abs(targetIndex - currentPageInfo.pageOffset) > currentPageInfo.pageSize + else { + let callback: () -> () = { + self?.load(.untilInclusive(id: targetId, padding: paddingForInclusive)) + } + return (nil, callback) + } + + // If the targetId is further than 1 pageSize away then discard the current + // cached data and trigger a fresh `initialPageAround` + let callback: () -> () = { + self?.dataCache.mutate { $0 = DataCache() } + self?.associatedRecords.forEach { $0.clearCache(db) } + self?.pageInfo.mutate { + $0 = PagedData.PageInfo( + pageSize: currentPageInfo.pageSize, + pageOffset: 0, + currentCount: 0, + totalCount: 0 + ) + } + self?.load(.initialPageAround(id: targetId)) + } + + return (nil, callback) + case .reloadCurrent: return ( - currentPageInfo.currentCount, - currentPageInfo.pageOffset, - currentPageInfo.pageOffset + ( + currentPageInfo.currentCount, + currentPageInfo.pageOffset, + currentPageInfo.pageOffset + ), + nil ) } }() // If there is no queryOffset then we already have the data we need so // early-out (may as well update the 'totalCount' since it may be relevant) - guard let queryInfo: (limit: Int, offset: Int, updatedCacheOffset: Int) = queryInfo else { + guard let queryInfo: QueryInfo = queryInfo else { return ( nil, PagedData.PageInfo( @@ -501,7 +614,8 @@ public class PagedDatabaseObserver: TransactionObserver where pageOffset: currentPageInfo.pageOffset, currentCount: currentPageInfo.currentCount, totalCount: totalCount - ) + ), + callback ) } @@ -540,7 +654,7 @@ public class PagedDatabaseObserver: TransactionObserver where ) } - return (newData, updatedLimitInfo) + return (newData, updatedLimitInfo, nil) } // Unwrap the updated data @@ -554,6 +668,7 @@ public class PagedDatabaseObserver: TransactionObserver where self.pageInfo.mutate { $0 = updatedPageInfo } } self.isLoadingMoreData.mutate { $0 = false } + loadedPage?.failureCallback?() return } @@ -561,7 +676,7 @@ public class PagedDatabaseObserver: TransactionObserver where var associatedLoadedData: DataCache = DataCache(items: loadedPageData) self.associatedRecords.forEach { record in - associatedLoadedData = record.attachAssociatedData(to: associatedLoadedData) + associatedLoadedData = record.updateAssociatedData(to: associatedLoadedData) } // Update the cache and pageInfo @@ -651,7 +766,8 @@ public protocol ErasedAssociatedRecord { pageInfo: PagedData.PageInfo ) -> Bool @discardableResult func updateCache(_ db: Database, rowIds: [Int64], hasOtherChanges: Bool) -> Bool - func attachAssociatedData(to unassociatedCache: DataCache) -> DataCache + func clearCache(_ db: Database) + func updateAssociatedData(to unassociatedCache: DataCache) -> DataCache } // MARK: - DataCache @@ -704,6 +820,8 @@ public struct DataCache { } public func upserting(items: [T]) -> DataCache { + guard !items.isEmpty else { return self } + var updatedData: [Int64: T] = self.data var updatedLookup: [AnyHashable: Int64] = self.lookup @@ -733,6 +851,7 @@ public enum PagedData { case pageBefore case pageAfter case untilInclusive(id: SQLExpression, padding: Int) + case jumpTo(id: SQLExpression, paddingForInclusive: Int) case reloadCurrent } @@ -755,6 +874,13 @@ public enum PagedData { /// the padding would mean more data should be loaded) case untilInclusive(id: ID, padding: Int) + /// This will jump to the specified id, loading a page around it and clearing out any + /// data that was previously cached + /// + /// **Note:** If the id is within 1 pageSize of the currently cached data then this + /// will behave as per the `untilInclusive(id:padding:)` type + case jumpTo(id: ID, paddingForInclusive: Int) + fileprivate var internalTarget: InternalTarget { switch self { case .initialPageAround(let id): return .initialPageAround(id: id.sqlExpression) @@ -762,6 +888,9 @@ public enum PagedData { case .pageAfter: return .pageAfter case .untilInclusive(let id, let padding): return .untilInclusive(id: id.sqlExpression, padding: padding) + + case .jumpTo(let id, let paddingForInclusive): + return .jumpTo(id: id.sqlExpression, paddingForInclusive: paddingForInclusive) } } } @@ -820,11 +949,13 @@ public enum PagedData { let tableName: String let kind: DatabaseEvent.Kind let rowId: Int64 + let pagedRowIdsForRelatedDeletion: [Int64]? - init(event: DatabaseEvent) { + init(event: DatabaseEvent, pagedRowIdsForRelatedDeletion: [Int64]? = nil) { self.tableName = event.tableName self.kind = event.kind self.rowId = event.rowID + self.pagedRowIdsForRelatedDeletion = pagedRowIdsForRelatedDeletion } } @@ -1144,7 +1275,11 @@ public class AssociatedRecord: ErasedAssociatedRecord where T: Fet return true } - public func attachAssociatedData(to unassociatedCache: DataCache) -> DataCache { + public func clearCache(_ db: Database) { + dataCache.mutate { $0 = DataCache() } + } + + public func updateAssociatedData(to unassociatedCache: DataCache) -> DataCache { guard let typedCache: DataCache = unassociatedCache as? DataCache else { return unassociatedCache } diff --git a/SessionUtilitiesKit/General/Dependencies.swift b/SessionUtilitiesKit/General/Dependencies.swift index 5ac20c999..a0c3f132c 100644 --- a/SessionUtilitiesKit/General/Dependencies.swift +++ b/SessionUtilitiesKit/General/Dependencies.swift @@ -3,28 +3,28 @@ import Foundation open class Dependencies { - public var _generalCache: Atomic? + public var _generalCache: Atomic?> public var generalCache: Atomic { get { Dependencies.getValueSettingIfNull(&_generalCache) { General.cache } } - set { _generalCache = newValue } + set { _generalCache.mutate { $0 = newValue } } } - public var _storage: Storage? + public var _storage: Atomic public var storage: Storage { get { Dependencies.getValueSettingIfNull(&_storage) { Storage.shared } } - set { _storage = newValue } + set { _storage.mutate { $0 = newValue } } } - public var _standardUserDefaults: UserDefaultsType? + public var _standardUserDefaults: Atomic public var standardUserDefaults: UserDefaultsType { get { Dependencies.getValueSettingIfNull(&_standardUserDefaults) { UserDefaults.standard } } - set { _standardUserDefaults = newValue } + set { _standardUserDefaults.mutate { $0 = newValue } } } - public var _date: Date? + public var _date: Atomic public var date: Date { get { Dependencies.getValueSettingIfNull(&_date) { Date() } } - set { _date = newValue } + set { _date.mutate { $0 = newValue } } } // MARK: - Initialization @@ -35,21 +35,29 @@ open class Dependencies { standardUserDefaults: UserDefaultsType? = nil, date: Date? = nil ) { - _generalCache = generalCache - _storage = storage - _standardUserDefaults = standardUserDefaults - _date = date + _generalCache = Atomic(generalCache) + _storage = Atomic(storage) + _standardUserDefaults = Atomic(standardUserDefaults) + _date = Atomic(date) } // MARK: - Convenience - - public static func getValueSettingIfNull(_ maybeValue: inout T?, _ valueGenerator: () -> T) -> T { - guard let value: T = maybeValue else { + + public static func getValueSettingIfNull(_ maybeValue: inout Atomic, _ valueGenerator: () -> T) -> T { + guard let value: T = maybeValue.wrappedValue else { let value: T = valueGenerator() - maybeValue = value + maybeValue.mutate { $0 = value } return value } return value } + +// 0 libswiftCore.dylib 0x00000001999fd40c _swift_release_dealloc + 32 (HeapObject.cpp:703) +// 1 SessionMessagingKit 0x0000000106aa958c 0x106860000 + 2397580 +// 2 libswiftCore.dylib 0x00000001999fd424 _swift_release_dealloc + 56 (HeapObject.cpp:703) +// 3 SessionUtilitiesKit 0x0000000106cbd980 static Dependencies.getValueSettingIfNull(_:_:) + 264 (Dependencies.swift:49) +// 4 SessionMessagingKit 0x0000000106aa90f4 closure #1 in SMKDependencies.sign.getter + 112 (SMKDependencies.swift:17) +// 5 SessionUtilitiesKit 0x0000000106cbd974 static Dependencies.getValueSettingIfNull(_:_:) + 252 (Dependencies.swift:48) +// 6 SessionMessagingKit 0x000000010697aef8 specialized static OpenGroupAPI.sign(_:messageBytes:for:fallbackSigningType:using:) + 1158904 (OpenGroupAPI.swift:1190) } diff --git a/SessionUtilitiesKit/General/General.swift b/SessionUtilitiesKit/General/General.swift index 7fd3487e0..714184ee3 100644 --- a/SessionUtilitiesKit/General/General.swift +++ b/SessionUtilitiesKit/General/General.swift @@ -6,11 +6,13 @@ import Curve25519Kit public protocol GeneralCacheType { var encodedPublicKey: String? { get set } + var recentReactionTimestamps: [Int64] { get set } } public enum General { public class Cache: GeneralCacheType { public var encodedPublicKey: String? = nil + public var recentReactionTimestamps: [Int64] = [] } public static var cache: Atomic = Atomic(Cache()) diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index c84a11283..d336f624b 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -36,6 +36,13 @@ public protocol JobExecutor { } public final class JobRunner { + public enum JobResult { + case succeeded + case failed + case deferred + case notFound + } + private static let blockingQueue: Atomic = Atomic( JobQueue( type: .blocking, @@ -103,6 +110,7 @@ public final class JobRunner { internal static var executorMap: Atomic<[Job.Variant: JobExecutor.Type]> = Atomic([:]) fileprivate static var perSessionJobsCompleted: Atomic> = Atomic([]) private static var hasCompletedInitialBecomeActive: Atomic = Atomic(false) + private static var shutdownBackgroundTask: Atomic = Atomic(nil) // MARK: - Configuration @@ -222,6 +230,14 @@ public final class JobRunner { } public static func appDidBecomeActive() { + // If we have a running "sutdownBackgroundTask" then we want to cancel it as otherwise it + // can result in the database being suspended and us being unable to interact with it at all + shutdownBackgroundTask.mutate { + $0?.cancel() + $0 = nil + } + + // Retrieve any jobs which should run when becoming active let hasCompletedInitialBecomeActive: Bool = JobRunner.hasCompletedInitialBecomeActive.wrappedValue let jobsToRun: [Job] = Storage.shared .read { db in @@ -259,9 +275,56 @@ public final class JobRunner { /// Calling this will clear the JobRunner queues and stop it from running new jobs, any currently executing jobs will continue to run /// though (this means if we suspend the database it's likely that any currently running jobs will fail to complete and fail to record their /// failure - they _should_ be picked up again the next time the app is launched) - public static func stopAndClearPendingJobs() { - queues.wrappedValue.values.forEach { queue in - queue.stopAndClearPendingJobs() + public static func stopAndClearPendingJobs( + exceptForVariant: Job.Variant? = nil, + onComplete: (() -> ())? = nil + ) { + // Stop all queues except for the one containing the `exceptForVariant` + queues.wrappedValue + .values + .filter { queue -> Bool in + guard let exceptForVariant: Job.Variant = exceptForVariant else { return true } + + return !queue.jobVariants.contains(exceptForVariant) + } + .forEach { $0.stopAndClearPendingJobs() } + + // Ensure the queue is actually running (if not the trigger the callback immediately) + guard + let exceptForVariant: Job.Variant = exceptForVariant, + let queue: JobQueue = queues.wrappedValue[exceptForVariant], + queue.isRunning.wrappedValue == true + else { + onComplete?() + return + } + + let oldQueueDrained: (() -> ())? = queue.onQueueDrained + + // Create a backgroundTask to give the queue the chance to properly be drained + shutdownBackgroundTask.mutate { + $0 = OWSBackgroundTask(labelStr: #function) { [weak queue] state in + // If the background task didn't succeed then trigger the onComplete (and hope we have + // enough time to complete it's logic) + guard state != .cancelled else { + queue?.onQueueDrained = oldQueueDrained + return + } + guard state != .success else { return } + + onComplete?() + queue?.onQueueDrained = oldQueueDrained + queue?.stopAndClearPendingJobs() + } + } + + // Add a callback to be triggered once the queue is drained + queue.onQueueDrained = { [weak queue] in + oldQueueDrained?() + queue?.onQueueDrained = oldQueueDrained + onComplete?() + + shutdownBackgroundTask.mutate { $0 = nil } } } @@ -276,6 +339,15 @@ public final class JobRunner { .defaulting(to: [:]) } + public static func afterCurrentlyRunningJob(_ job: Job?, callback: @escaping (JobResult) -> ()) { + guard let job: Job = job, let jobId: Int64 = job.id, let queue: JobQueue = queues.wrappedValue[job.variant] else { + callback(.notFound) + return + } + + queue.afterCurrentlyRunningJob(jobId, callback: callback) + } + public static func hasPendingOrRunningJob(with variant: Job.Variant, details: T) -> Bool { guard let targetQueue: JobQueue = queues.wrappedValue[variant] else { return false } guard let detailsData: Data = try? JSONEncoder().encode(details) else { return false } @@ -283,6 +355,12 @@ public final class JobRunner { return targetQueue.hasPendingOrRunningJob(with: detailsData) } + public static func removePendingJob(_ job: Job?) { + guard let job: Job = job, let jobId: Int64 = job.id else { return } + + queues.wrappedValue[job.variant]?.removePendingJob(jobId) + } + // MARK: - Convenience fileprivate static func getRetryInterval(for job: Job) -> TimeInterval { @@ -370,7 +448,7 @@ private final class JobQueue { /// The specific types of jobs this queue manages, if this is left empty it will handle all jobs not handled by other queues fileprivate let jobVariants: [Job.Variant] - private let onQueueDrained: (() -> ())? + fileprivate var onQueueDrained: (() -> ())? private lazy var internalQueue: DispatchQueue = { let result: DispatchQueue = DispatchQueue( @@ -389,6 +467,7 @@ private final class JobQueue { fileprivate var isRunning: Atomic = Atomic(false) private var queue: Atomic<[Job]> = Atomic([]) private var jobsCurrentlyRunning: Atomic> = Atomic([]) + private var jobCallbacks: Atomic<[Int64: [(JobRunner.JobResult) -> ()]]> = Atomic([:]) private var detailsForCurrentlyRunningJobs: Atomic<[Int64: Data?]> = Atomic([:]) private var deferLoopTracker: Atomic<[Int64: (count: Int, times: [TimeInterval])]> = Atomic([:]) @@ -504,12 +583,29 @@ private final class JobQueue { return detailsForCurrentlyRunningJobs.wrappedValue } + fileprivate func afterCurrentlyRunningJob(_ jobId: Int64, callback: @escaping (JobRunner.JobResult) -> ()) { + guard isCurrentlyRunning(jobId) else { + callback(.notFound) + return + } + + jobCallbacks.mutate { jobCallbacks in + jobCallbacks[jobId] = (jobCallbacks[jobId] ?? []).appending(callback) + } + } + fileprivate func hasPendingOrRunningJob(with detailsData: Data?) -> Bool { let pendingJobs: [Job] = queue.wrappedValue return pendingJobs.contains { job in job.details == detailsData } } + fileprivate func removePendingJob(_ jobId: Int64) { + queue.mutate { queue in + queue = queue.filter { $0.id != jobId } + } + } + // MARK: - Job Running fileprivate func start(force: Bool = false) { @@ -844,10 +940,8 @@ private final class JobQueue { } } - // The job is removed from the queue before it runs so all we need to to is remove it - // from the 'currentlyRunning' set and start the next one - jobsCurrentlyRunning.mutate { $0 = $0.removing(job.id) } - detailsForCurrentlyRunningJobs.mutate { $0 = $0.removingValue(forKey: job.id) } + // Perform job cleanup and start the next job + performCleanUp(for: job, result: .succeeded) internalQueue.async { [weak self] in self?.runNextJob() } @@ -858,8 +952,7 @@ private final class JobQueue { private func handleJobFailed(_ job: Job, error: Error?, permanentFailure: Bool) { guard Storage.shared.read({ db in try Job.exists(db, id: job.id ?? -1) }) == true else { SNLog("[JobRunner] \(queueContext) \(job.variant) job canceled") - jobsCurrentlyRunning.mutate { $0 = $0.removing(job.id) } - detailsForCurrentlyRunningJobs.mutate { $0 = $0.removingValue(forKey: job.id) } + performCleanUp(for: job, result: .failed) internalQueue.async { [weak self] in self?.runNextJob() @@ -867,12 +960,30 @@ private final class JobQueue { return } - // If this is the blocking queue and a "blocking" job failed then rerun it immediately + // If this is the blocking queue and a "blocking" job failed then rerun it + // immediately (in this case we don't trigger any job callbacks because the + // job isn't actually done, it's going to try again immediately) if self.type == .blocking && job.shouldBlock { SNLog("[JobRunner] \(queueContext) \(job.variant) job failed; retrying immediately") - jobsCurrentlyRunning.mutate { $0 = $0.removing(job.id) } - detailsForCurrentlyRunningJobs.mutate { $0 = $0.removingValue(forKey: job.id) } - queue.mutate { $0.insert(job, at: 0) } + + // If it was a possible deferral loop then we don't actually want to + // retry the job (even if it's a blocking one, this gives a small chance + // that the app could continue to function) + let wasPossibleDeferralLoop: Bool = { + if let error = error, case JobRunnerError.possibleDeferralLoop = error { return true } + + return false + }() + performCleanUp( + for: job, + result: .failed, + shouldTriggerCallbacks: wasPossibleDeferralLoop + ) + + // Only add it back to the queue if it wasn't a deferral loop + if !wasPossibleDeferralLoop { + queue.mutate { $0.insert(job, at: 0) } + } internalQueue.async { [weak self] in self?.runNextJob() @@ -947,8 +1058,7 @@ private final class JobQueue { } } - jobsCurrentlyRunning.mutate { $0 = $0.removing(job.id) } - detailsForCurrentlyRunningJobs.mutate { $0 = $0.removingValue(forKey: job.id) } + performCleanUp(for: job, result: .failed) internalQueue.async { [weak self] in self?.runNextJob() } @@ -958,8 +1068,7 @@ private final class JobQueue { /// on other jobs, and it should automatically manage those dependencies) private func handleJobDeferred(_ job: Job) { var stuckInDeferLoop: Bool = false - jobsCurrentlyRunning.mutate { $0 = $0.removing(job.id) } - detailsForCurrentlyRunningJobs.mutate { $0 = $0.removingValue(forKey: job.id) } + deferLoopTracker.mutate { guard let lastRecord: (count: Int, times: [TimeInterval]) = $0[job.id] else { $0 = $0.setting( @@ -999,8 +1108,29 @@ private final class JobQueue { return } + performCleanUp(for: job, result: .deferred) internalQueue.async { [weak self] in self?.runNextJob() } } + + private func performCleanUp(for job: Job, result: JobRunner.JobResult, shouldTriggerCallbacks: Bool = true) { + // The job is removed from the queue before it runs so all we need to to is remove it + // from the 'currentlyRunning' set + jobsCurrentlyRunning.mutate { $0 = $0.removing(job.id) } + detailsForCurrentlyRunningJobs.mutate { $0 = $0.removingValue(forKey: job.id) } + + guard shouldTriggerCallbacks else { return } + + // Run any job callbacks now that it's done + var jobCallbacksToRun: [(JobRunner.JobResult) -> ()] = [] + jobCallbacks.mutate { jobCallbacks in + jobCallbacksToRun = (jobCallbacks[job.id] ?? []) + jobCallbacks = jobCallbacks.removingValue(forKey: job.id) + } + + DispatchQueue.global(qos: .default).async { + jobCallbacksToRun.forEach { $0(result) } + } + } } diff --git a/SessionUtilitiesKit/Meta/SessionUtilitiesKit.h b/SessionUtilitiesKit/Meta/SessionUtilitiesKit.h index 8624165de..1a6f9e631 100644 --- a/SessionUtilitiesKit/Meta/SessionUtilitiesKit.h +++ b/SessionUtilitiesKit/Meta/SessionUtilitiesKit.h @@ -15,4 +15,5 @@ FOUNDATION_EXPORT const unsigned char SessionUtilitiesKitVersionString[]; #import #import #import +#import diff --git a/SessionMessagingKit/Utilities/OWSBackgroundTask.h b/SessionUtilitiesKit/Utilities/OWSBackgroundTask.h similarity index 97% rename from SessionMessagingKit/Utilities/OWSBackgroundTask.h rename to SessionUtilitiesKit/Utilities/OWSBackgroundTask.h index 70a9fbdd0..73e632cd6 100644 --- a/SessionMessagingKit/Utilities/OWSBackgroundTask.h +++ b/SessionUtilitiesKit/Utilities/OWSBackgroundTask.h @@ -10,6 +10,7 @@ typedef NS_ENUM(NSUInteger, BackgroundTaskState) { BackgroundTaskState_Success, BackgroundTaskState_CouldNotStart, BackgroundTaskState_Expired, + BackgroundTaskState_Cancelled, }; typedef void (^BackgroundTaskCompletionBlock)(BackgroundTaskState backgroundTaskState); @@ -57,6 +58,8 @@ typedef void (^BackgroundTaskCompletionBlock)(BackgroundTaskState backgroundTask + (OWSBackgroundTask *)backgroundTaskWithLabel:(NSString *)label completionBlock:(BackgroundTaskCompletionBlock)completionBlock; +- (void)cancel; + @end NS_ASSUME_NONNULL_END diff --git a/SessionMessagingKit/Utilities/OWSBackgroundTask.m b/SessionUtilitiesKit/Utilities/OWSBackgroundTask.m similarity index 95% rename from SessionMessagingKit/Utilities/OWSBackgroundTask.m rename to SessionUtilitiesKit/Utilities/OWSBackgroundTask.m index 3ce898431..7602df9e6 100644 --- a/SessionMessagingKit/Utilities/OWSBackgroundTask.m +++ b/SessionUtilitiesKit/Utilities/OWSBackgroundTask.m @@ -375,6 +375,31 @@ typedef NSNumber *OWSTaskId; } } +- (void)cancel +{ + // Make a local copy of this state, since this method is called by `dealloc`. + BackgroundTaskCompletionBlock _Nullable completionBlock; + + @synchronized(self) + { + if (!self.taskId) { + return; + } + [OWSBackgroundTaskManager.sharedManager removeTask:self.taskId]; + self.taskId = nil; + + completionBlock = self.completionBlock; + self.completionBlock = nil; + } + + // endBackgroundTask must be called on the main thread. + DispatchMainThreadSafe(^{ + if (completionBlock) { + completionBlock(BackgroundTaskState_Cancelled); + } + }); +} + - (void)endBackgroundTask { // Make a local copy of this state, since this method is called by `dealloc`. diff --git a/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift b/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift index bb8b477b1..142f606e9 100644 --- a/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift +++ b/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift @@ -57,10 +57,23 @@ public final class ProfilePictureView: UIView { return result }() + private lazy var additionalProfilePlaceholderImageView: UIImageView = { + let result: UIImageView = UIImageView( + image: UIImage(systemName: "person.fill")?.withRenderingMode(.alwaysTemplate) + ) + result.translatesAutoresizingMaskIntoConstraints = false + result.contentMode = .scaleAspectFill + result.tintColor = Colors.text + result.isHidden = true + + return result + }() + private lazy var additionalImageView: UIImageView = { let result: UIImageView = UIImageView() result.translatesAutoresizingMaskIntoConstraints = false result.contentMode = .scaleAspectFill + result.tintColor = Colors.text result.isHidden = true return result @@ -107,11 +120,17 @@ public final class ProfilePictureView: UIView { imageContainerView.addSubview(animatedImageView) additionalImageContainerView.addSubview(additionalImageView) additionalImageContainerView.addSubview(additionalAnimatedImageView) + additionalImageContainerView.addSubview(additionalProfilePlaceholderImageView) imageView.pin(to: imageContainerView) animatedImageView.pin(to: imageContainerView) additionalImageView.pin(to: additionalImageContainerView) additionalAnimatedImageView.pin(to: additionalImageContainerView) + + additionalProfilePlaceholderImageView.pin(.top, to: .top, of: additionalImageContainerView, withInset: 3) + additionalProfilePlaceholderImageView.pin(.left, to: .left, of: additionalImageContainerView) + additionalProfilePlaceholderImageView.pin(.right, to: .right, of: additionalImageContainerView) + additionalProfilePlaceholderImageView.pin(.bottom, to: .bottom, of: additionalImageContainerView, withInset: 3) } // FIXME: Remove this once we refactor the OWSConversationSettingsViewController to Swift (use the HomeViewModel approach) @@ -172,6 +191,7 @@ public final class ProfilePictureView: UIView { additionalAnimatedImageView.image = nil additionalImageView.isHidden = true additionalAnimatedImageView.isHidden = true + additionalProfilePlaceholderImageView.isHidden = true return } guard !publicKey.isEmpty || openGroupProfilePictureData != nil else { return } @@ -240,6 +260,12 @@ public final class ProfilePictureView: UIView { additionalAnimatedImageView.image = animatedImage additionalImageView.isHidden = (animatedImage != nil) additionalAnimatedImageView.isHidden = (animatedImage == nil) + additionalProfilePlaceholderImageView.isHidden = true + } + else { + additionalImageView.isHidden = true + additionalAnimatedImageView.isHidden = true + additionalProfilePlaceholderImageView.isHidden = false } default: @@ -251,6 +277,7 @@ public final class ProfilePictureView: UIView { additionalImageView.isHidden = true additionalAnimatedImageView.image = nil additionalAnimatedImageView.isHidden = true + additionalProfilePlaceholderImageView.isHidden = true } // Set the image diff --git a/SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift b/SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift index 76b31539a..f6a399a6f 100644 --- a/SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift +++ b/SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift @@ -7,7 +7,7 @@ import SessionMessagingKit public class NoopNotificationsManager: NotificationsProtocol { public init() {} - public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, isBackgroundPoll: Bool) { + public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread) { owsFailDebug("") } @@ -15,6 +15,10 @@ public class NoopNotificationsManager: NotificationsProtocol { owsFailDebug("") } + public func notifyUser(_ db: Database, forReaction reaction: Reaction, in thread: SessionThread) { + owsFailDebug("") + } + public func cancelNotifications(identifiers: [String]) { owsFailDebug("") } diff --git a/SignalUtilitiesKit/Utilities/OrderedDictionary.swift b/SignalUtilitiesKit/Utilities/OrderedDictionary.swift index 5cdd820c1..5e38f9e4d 100644 --- a/SignalUtilitiesKit/Utilities/OrderedDictionary.swift +++ b/SignalUtilitiesKit/Utilities/OrderedDictionary.swift @@ -102,4 +102,16 @@ public class OrderedDictionary { } return values } + + public var orderedItems: [(KeyType, ValueType)] { + var items = [(KeyType, ValueType)]() + for key in orderedKeys { + guard let value = self.keyValueMap[key] else { + owsFailDebug("Missing value") + continue + } + items.append((key, value)) + } + return items + } }